From c646459204eb02cb5f796421a8b846e83d3adf7d Mon Sep 17 00:00:00 2001 From: hayanesuru Date: Mon, 2 Feb 2026 13:53:36 +0900 Subject: [PATCH] async pathfinding: fix path race condition #519 #589 Closed #604 --- .../0102-Petal-Async-Pathfinding.patch | 11 ++- .../org/dreeam/leaf/async/path/AsyncPath.java | 83 +++++++++---------- .../leaf/async/path/AsyncPathProcessor.java | 4 +- 3 files changed, 48 insertions(+), 50 deletions(-) diff --git a/leaf-server/minecraft-patches/features/0102-Petal-Async-Pathfinding.patch b/leaf-server/minecraft-patches/features/0102-Petal-Async-Pathfinding.patch index 9716b3a46..cedd24243 100644 --- a/leaf-server/minecraft-patches/features/0102-Petal-Async-Pathfinding.patch +++ b/leaf-server/minecraft-patches/features/0102-Petal-Async-Pathfinding.patch @@ -728,18 +728,21 @@ index 6c5696ab4981a6d582d4d0f13c9822bf84a5d9f1..5cd93a95091deb045b24c23d75a35a85 @Override diff --git a/net/minecraft/world/level/pathfinder/Path.java b/net/minecraft/world/level/pathfinder/Path.java -index 5959e1b1772ffbdfb108365171fe37cbf56ef825..68723bebf60bdb8faa243058e8b0d584cb9a2177 100644 +index 5959e1b1772ffbdfb108365171fe37cbf56ef825..701a3158c3e078e558fb1d3e11b6c40c7778561c 100644 --- a/net/minecraft/world/level/pathfinder/Path.java +++ b/net/minecraft/world/level/pathfinder/Path.java -@@ -11,7 +11,7 @@ import net.minecraft.world.entity.Entity; +@@ -11,9 +11,9 @@ import net.minecraft.world.entity.Entity; import net.minecraft.world.phys.Vec3; import org.jspecify.annotations.Nullable; -public final class Path { -+public class Path { // Kaiiju - petal - async path processing - not final ++public class Path { // Kaiiju - petal - async path processing - public -> public-f public static final StreamCodec STREAM_CODEC = StreamCodec.of((buffer, value) -> value.writeToStream(buffer), Path::createFromStream); - public final List nodes; +- public final List nodes; ++ public List nodes; // Kaiiju - petal - async path processing - public -> public-f private Path.@Nullable DebugData debugData; + private int nextNodeIndex; + private final BlockPos target; @@ -27,6 +27,17 @@ public final class Path { this.reached = reached; } diff --git a/leaf-server/src/main/java/org/dreeam/leaf/async/path/AsyncPath.java b/leaf-server/src/main/java/org/dreeam/leaf/async/path/AsyncPath.java index 9f9e89eec..16a9f50cb 100644 --- a/leaf-server/src/main/java/org/dreeam/leaf/async/path/AsyncPath.java +++ b/leaf-server/src/main/java/org/dreeam/leaf/async/path/AsyncPath.java @@ -10,6 +10,7 @@ import org.jspecify.annotations.Nullable; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.function.Supplier; @@ -17,13 +18,9 @@ /** * I'll be using this to represent a path that not be processed yet! */ -public class AsyncPath extends Path { +public final class AsyncPath extends Path implements Runnable { - /** - * Instead of three states, only one is actually required - * This will update when any thread is done with the path - */ - private volatile boolean ready = false; + private boolean ready = false; /** * Runnable waiting for this to be processed @@ -36,19 +33,9 @@ public class AsyncPath extends Path { */ private final Set positions; - /** - * The supplier of the real processed path - */ - private final Supplier pathSupplier; - - /* - * Processed values - */ + private @Nullable Supplier task; + private volatile @Nullable Path ret; - /** - * This is a reference to the nodes list in the parent `Path` object - */ - private final List nodes; /** * The block we're trying to path to *

@@ -72,13 +59,20 @@ public class AsyncPath extends Path { public AsyncPath(List emptyNodeList, Set positions, Supplier pathSupplier) { super(emptyNodeList, null, false); - this.nodes = emptyNodeList; this.positions = positions; - this.pathSupplier = pathSupplier; + this.task = pathSupplier; AsyncPathProcessor.queue(this); } + @Override + public void run() { + Supplier task = this.task; + if (task != null) { + this.ret = task.get(); + } + } + @Override public boolean isProcessed() { return this.ready; @@ -92,9 +86,6 @@ public final void schedulePostProcessing(Runnable runnable) { runnable.run(); } else { this.postProcessing.offer(runnable); - if (this.ready) { - this.runAllPostProcessing(true); - } } } @@ -105,34 +96,31 @@ public final void schedulePostProcessing(Runnable runnable) { * @return true if we are processing the same positions */ public final boolean hasSameProcessingPositions(final Set positions) { - if (this.positions.size() != positions.size()) { - return false; - } - - // For single position (common case), do direct comparison - if (positions.size() == 1) { // Both have the same size at this point - return this.positions.iterator().next().equals(positions.iterator().next()); - } - - return this.positions.containsAll(positions); + return this.positions.equals(positions); } /** * Starts processing this path * Since this is no longer a synchronized function, checkProcessed is no longer required */ - public final void process() { - if (this.ready) return; - - synchronized (this) { - if (this.ready) return; // In the worst case, the main thread only waits until any async thread is done and returns immediately - final Path bestPath = this.pathSupplier.get(); - this.nodes.addAll(bestPath.nodes); // We mutate this list to reuse the logic in Path - this.target = bestPath.getTarget(); - this.distToTarget = bestPath.getDistToTarget(); - this.canReach = bestPath.canReach(); - this.ready = true; + private final void process() { + if (this.ready) { + return; } + final Path ret = this.ret; + final Supplier task = this.task; + final Path bestPath = ret != null ? ret : Objects.requireNonNull(task).get(); + complete(bestPath); + } + + // not this.ready + private final void complete(Path bestPath) { + this.nodes = bestPath.nodes; + this.target = bestPath.getTarget(); + this.distToTarget = bestPath.getDistToTarget(); + this.canReach = bestPath.canReach(); + this.task = null; + this.ready = true; this.runAllPostProcessing(TickThread.isTickThread()); } @@ -176,6 +164,13 @@ public boolean canReach() { @Override public boolean isDone() { + boolean ready = this.ready; + if (!ready) { + Path ret = this.ret; + if (ret != null) { + complete(ret); + } + } return this.ready && super.isDone(); } diff --git a/leaf-server/src/main/java/org/dreeam/leaf/async/path/AsyncPathProcessor.java b/leaf-server/src/main/java/org/dreeam/leaf/async/path/AsyncPathProcessor.java index 255829a21..7ee5fa0eb 100644 --- a/leaf-server/src/main/java/org/dreeam/leaf/async/path/AsyncPathProcessor.java +++ b/leaf-server/src/main/java/org/dreeam/leaf/async/path/AsyncPathProcessor.java @@ -46,8 +46,8 @@ public static void init() { } } - protected static CompletableFuture queue(AsyncPath path) { - return CompletableFuture.runAsync(path::process, PATH_PROCESSING_EXECUTOR) + protected static CompletableFuture queue(Runnable path) { + return CompletableFuture.runAsync(path, PATH_PROCESSING_EXECUTOR) .orTimeout(60L, TimeUnit.SECONDS) .exceptionally(throwable -> { if (throwable instanceof TimeoutException e) {