Skip to content

Commit 2f2822a

Browse files
committed
async pathfinding: fix path race condition
#519 #589 Closed #604
1 parent 8e2e5b0 commit 2f2822a

3 files changed

Lines changed: 49 additions & 51 deletions

File tree

leaf-server/minecraft-patches/features/0102-Petal-Async-Pathfinding.patch

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -728,18 +728,21 @@ index 6c5696ab4981a6d582d4d0f13c9822bf84a5d9f1..5cd93a95091deb045b24c23d75a35a85
728728

729729
@Override
730730
diff --git a/net/minecraft/world/level/pathfinder/Path.java b/net/minecraft/world/level/pathfinder/Path.java
731-
index 5959e1b1772ffbdfb108365171fe37cbf56ef825..68723bebf60bdb8faa243058e8b0d584cb9a2177 100644
731+
index 5959e1b1772ffbdfb108365171fe37cbf56ef825..701a3158c3e078e558fb1d3e11b6c40c7778561c 100644
732732
--- a/net/minecraft/world/level/pathfinder/Path.java
733733
+++ b/net/minecraft/world/level/pathfinder/Path.java
734-
@@ -11,7 +11,7 @@ import net.minecraft.world.entity.Entity;
734+
@@ -11,9 +11,9 @@ import net.minecraft.world.entity.Entity;
735735
import net.minecraft.world.phys.Vec3;
736736
import org.jspecify.annotations.Nullable;
737737

738738
-public final class Path {
739-
+public class Path { // Kaiiju - petal - async path processing - not final
739+
+public class Path { // Kaiiju - petal - async path processing - public -> public-f
740740
public static final StreamCodec<FriendlyByteBuf, Path> STREAM_CODEC = StreamCodec.of((buffer, value) -> value.writeToStream(buffer), Path::createFromStream);
741-
public final List<Node> nodes;
741+
- public final List<Node> nodes;
742+
+ public List<Node> nodes; // Kaiiju - petal - async path processing - public -> public-f
742743
private Path.@Nullable DebugData debugData;
744+
private int nextNodeIndex;
745+
private final BlockPos target;
743746
@@ -27,6 +27,17 @@ public final class Path {
744747
this.reached = reached;
745748
}

leaf-server/src/main/java/org/dreeam/leaf/async/path/AsyncPath.java

Lines changed: 40 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,17 @@
1010
import org.jspecify.annotations.Nullable;
1111

1212
import java.util.List;
13+
import java.util.Objects;
1314
import java.util.Set;
1415
import java.util.concurrent.ConcurrentLinkedQueue;
1516
import java.util.function.Supplier;
1617

1718
/**
1819
* I'll be using this to represent a path that not be processed yet!
1920
*/
20-
public class AsyncPath extends Path {
21+
public final class AsyncPath extends Path implements Runnable {
2122

22-
/**
23-
* Instead of three states, only one is actually required
24-
* This will update when any thread is done with the path
25-
*/
26-
private volatile boolean ready = false;
23+
private boolean ready = false;
2724

2825
/**
2926
* Runnable waiting for this to be processed
@@ -36,19 +33,9 @@ public class AsyncPath extends Path {
3633
*/
3734
private final Set<BlockPos> positions;
3835

39-
/**
40-
* The supplier of the real processed path
41-
*/
42-
private final Supplier<Path> pathSupplier;
43-
44-
/*
45-
* Processed values
46-
*/
36+
private @Nullable Supplier<Path> task;
37+
private volatile @Nullable Path ret;
4738

48-
/**
49-
* This is a reference to the nodes list in the parent `Path` object
50-
*/
51-
private final List<Node> nodes;
5239
/**
5340
* The block we're trying to path to
5441
* <p>
@@ -72,13 +59,20 @@ public class AsyncPath extends Path {
7259
public AsyncPath(List<Node> emptyNodeList, Set<BlockPos> positions, Supplier<Path> pathSupplier) {
7360
super(emptyNodeList, null, false);
7461

75-
this.nodes = emptyNodeList;
7662
this.positions = positions;
77-
this.pathSupplier = pathSupplier;
63+
this.task = pathSupplier;
7864

7965
AsyncPathProcessor.queue(this);
8066
}
8167

68+
@Override
69+
public void run() {
70+
Supplier<Path> task = this.task;
71+
if (task != null) {
72+
this.ret = task.get();
73+
}
74+
}
75+
8276
@Override
8377
public boolean isProcessed() {
8478
return this.ready;
@@ -92,9 +86,6 @@ public final void schedulePostProcessing(Runnable runnable) {
9286
runnable.run();
9387
} else {
9488
this.postProcessing.offer(runnable);
95-
if (this.ready) {
96-
this.runAllPostProcessing(true);
97-
}
9889
}
9990
}
10091

@@ -105,34 +96,31 @@ public final void schedulePostProcessing(Runnable runnable) {
10596
* @return true if we are processing the same positions
10697
*/
10798
public final boolean hasSameProcessingPositions(final Set<BlockPos> positions) {
108-
if (this.positions.size() != positions.size()) {
109-
return false;
110-
}
111-
112-
// For single position (common case), do direct comparison
113-
if (positions.size() == 1) { // Both have the same size at this point
114-
return this.positions.iterator().next().equals(positions.iterator().next());
115-
}
116-
117-
return this.positions.containsAll(positions);
99+
return this.positions.equals(positions);
118100
}
119101

120102
/**
121103
* Starts processing this path
122104
* Since this is no longer a synchronized function, checkProcessed is no longer required
123105
*/
124-
public final void process() {
125-
if (this.ready) return;
126-
127-
synchronized (this) {
128-
if (this.ready) return; // In the worst case, the main thread only waits until any async thread is done and returns immediately
129-
final Path bestPath = this.pathSupplier.get();
130-
this.nodes.addAll(bestPath.nodes); // We mutate this list to reuse the logic in Path
131-
this.target = bestPath.getTarget();
132-
this.distToTarget = bestPath.getDistToTarget();
133-
this.canReach = bestPath.canReach();
134-
this.ready = true;
106+
private final void process() {
107+
if (this.ready) {
108+
return;
135109
}
110+
final Path ret = this.ret;
111+
final Supplier<Path> task = this.task;
112+
final Path bestPath = ret != null ? ret : Objects.requireNonNull(task).get();
113+
complete(bestPath);
114+
}
115+
116+
// not this.ready
117+
private final void complete(Path bestPath) {
118+
this.nodes = bestPath.nodes;
119+
this.target = bestPath.getTarget();
120+
this.distToTarget = bestPath.getDistToTarget();
121+
this.canReach = bestPath.canReach();
122+
this.task = null;
123+
this.ready = true;
136124

137125
this.runAllPostProcessing(TickThread.isTickThread());
138126
}
@@ -176,7 +164,14 @@ public boolean canReach() {
176164

177165
@Override
178166
public boolean isDone() {
179-
return this.ready && super.isDone();
167+
boolean ready = this.ready;
168+
if (!ready) {
169+
Path ret = this.ret;
170+
if (ret != null) {
171+
complete(ret);
172+
}
173+
}
174+
return ready && super.isDone();
180175
}
181176

182177
@Override

leaf-server/src/main/java/org/dreeam/leaf/async/path/AsyncPathProcessor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ public static void init() {
4646
}
4747
}
4848

49-
protected static CompletableFuture<Void> queue(AsyncPath path) {
50-
return CompletableFuture.runAsync(path::process, PATH_PROCESSING_EXECUTOR)
49+
protected static CompletableFuture<Void> queue(Runnable path) {
50+
return CompletableFuture.runAsync(path, PATH_PROCESSING_EXECUTOR)
5151
.orTimeout(60L, TimeUnit.SECONDS)
5252
.exceptionally(throwable -> {
5353
if (throwable instanceof TimeoutException e) {

0 commit comments

Comments
 (0)