9
0
mirror of https://github.com/Winds-Studio/Leaf.git synced 2025-12-25 01:49:16 +00:00

Simplified AsyncPath (#423)

* Some improvements for Async Path

* Fix small race condition, thx to @MartijnMuijsers

* This @Nullable is from an old implementation and no longer required
This commit is contained in:
𝑩𝒊𝒒𝒖𝒂𝒕𝒆𝒓𝒏𝒊𝒐𝒏𝒔
2025-07-26 11:31:11 -05:00
committed by GitHub
parent fd2f6e6bef
commit 42caad5a46
3 changed files with 89 additions and 113 deletions

View File

@@ -1,6 +1,8 @@
package org.dreeam.leaf.async.path;
import ca.spottedleaf.moonrise.common.util.TickThread;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.pathfinder.Node;
import net.minecraft.world.level.pathfinder.Path;
@@ -8,33 +10,35 @@ import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Supplier;
/**
* i'll be using this to represent a path that not be processed yet!
* I'll be using this to represent a path that not be processed yet!
*/
public class AsyncPath extends Path {
/**
* marks whether this async path has been processed
* Instead of three states, only one is actually required
* This will update when any thread is done with the path
*/
private volatile PathProcessState processState = PathProcessState.WAITING;
private volatile boolean ready = false;
/**
* runnables waiting for this to be processed
* Runnable waiting for this to be processed
* ConcurrentLinkedQueue is thread-safe, non-blocking and non-synchronized
*/
private final List<Runnable> postProcessing = new ArrayList<>(0);
private final ConcurrentLinkedQueue<Runnable> postProcessing = new ConcurrentLinkedQueue<>();
/**
* a list of positions that this path could path towards
* A list of positions that this path could path towards
*/
private final Set<BlockPos> positions;
/**
* the supplier of the real processed path
* The supplier of the real processed path
*/
private final Supplier<Path> pathSupplier;
@@ -43,30 +47,30 @@ public class AsyncPath extends Path {
*/
/**
* this is a reference to the nodes list in the parent `Path` object
* This is a reference to the nodes list in the parent `Path` object
*/
private final List<Node> nodes;
/**
* the block we're trying to path to
* The block we're trying to path to
* <p>
* while processing, we have no idea where this is so consumers of `Path` should check that the path is processed before checking the target block
* While processing, we have no idea where this is so consumers of `Path` should check that the path is processed before checking the target block
*/
private @Nullable BlockPos target;
private BlockPos target;
/**
* how far we are to the target
* How far we are to the target
* <p>
* while processing, the target could be anywhere but theoretically we're always "close" to a theoretical target so default is 0
* While processing, the target could be anywhere, but theoretically we're always "close" to a theoretical target so default is 0
*/
private float distToTarget = 0;
/**
* whether we can reach the target
* Whether we can reach the target
* <p>
* while processing, we can always theoretically reach the target so default is true
* While processing, we can always theoretically reach the target so default is true
*/
private boolean canReach = true;
@SuppressWarnings("ConstantConditions")
public AsyncPath(@NotNull List<Node> emptyNodeList, @NotNull Set<BlockPos> positions, @NotNull Supplier<Path> pathSupplier) {
//noinspection ConstantConditions
super(emptyNodeList, null, false);
this.nodes = emptyNodeList;
@@ -78,211 +82,192 @@ public class AsyncPath extends Path {
@Override
public boolean isProcessed() {
return this.processState == PathProcessState.COMPLETED;
return this.ready;
}
/**
* returns the future representing the processing state of this path
* Returns the future representing the processing state of this path
*/
public synchronized void postProcessing(@NotNull Runnable runnable) {
if (isProcessed()) {
public final void schedulePostProcessing(@NotNull Runnable runnable) {
if (this.ready) {
runnable.run();
} else {
this.postProcessing.add(runnable);
this.postProcessing.offer(runnable);
if (this.ready) {
this.runAllPostProcessing(true);
}
}
}
/**
* an easy way to check if this processing path is the same as an attempted new path
* An easy way to check if this processing path is the same as an attempted new path
*
* @param positions - the positions to compare against
* @return true if we are processing the same positions
*/
public boolean hasSameProcessingPositions(final Set<BlockPos> positions) {
public final boolean hasSameProcessingPositions(final Set<BlockPos> 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);
}
/**
* starts processing this path
* Starts processing this path
* Since this is no longer a synchronized function, checkProcessed is no longer required
*/
public synchronized void process() {
if (this.processState == PathProcessState.COMPLETED ||
this.processState == PathProcessState.PROCESSING) {
return;
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;
}
processState = PathProcessState.PROCESSING;
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();
processState = PathProcessState.COMPLETED;
for (Runnable runnable : this.postProcessing) {
runnable.run();
} // Run tasks after processing
this.runAllPostProcessing(TickThread.isTickThread());
}
/**
* if this path is accessed while it hasn't processed, just process it in-place
*/
private void checkProcessed() {
if (this.processState == PathProcessState.WAITING ||
this.processState == PathProcessState.PROCESSING) { // Block if we are on processing
this.process();
private void runAllPostProcessing(boolean isTickThread) {
Runnable runnable;
while ((runnable = this.postProcessing.poll()) != null) {
if (isTickThread) {
runnable.run();
} else {
MinecraftServer.getServer().scheduleOnMain(runnable);
}
}
}
/*
* overrides we need for final fields that we cannot modify after processing
* Overrides we need for final fields that we cannot modify after processing
*/
@Override
public @NotNull BlockPos getTarget() {
this.checkProcessed();
this.process();
return this.target;
}
@Override
public float getDistToTarget() {
this.checkProcessed();
this.process();
return this.distToTarget;
}
@Override
public boolean canReach() {
this.checkProcessed();
this.process();
return this.canReach;
}
/*
* overrides to ensure we're processed first
* Overrides to ensure we're processed first
*/
@Override
public boolean isDone() {
return this.processState == PathProcessState.COMPLETED && super.isDone();
return this.ready && super.isDone();
}
@Override
public void advance() {
this.checkProcessed();
this.process();
super.advance();
}
@Override
public boolean notStarted() {
this.checkProcessed();
this.process();
return super.notStarted();
}
@Nullable
@Override
public Node getEndNode() {
this.checkProcessed();
public @Nullable Node getEndNode() {
this.process();
return super.getEndNode();
}
@Override
public Node getNode(int index) {
this.checkProcessed();
public @NotNull Node getNode(int index) {
this.process();
return super.getNode(index);
}
@Override
public void truncateNodes(int length) {
this.checkProcessed();
this.process();
super.truncateNodes(length);
}
@Override
public void replaceNode(int index, Node node) {
this.checkProcessed();
public void replaceNode(int index, @NotNull Node node) {
this.process();
super.replaceNode(index, node);
}
@Override
public int getNodeCount() {
this.checkProcessed();
this.process();
return super.getNodeCount();
}
@Override
public int getNextNodeIndex() {
this.checkProcessed();
this.process();
return super.getNextNodeIndex();
}
@Override
public void setNextNodeIndex(int nodeIndex) {
this.checkProcessed();
this.process();
super.setNextNodeIndex(nodeIndex);
}
@Override
public Vec3 getEntityPosAtNode(Entity entity, int index) {
this.checkProcessed();
public @NotNull Vec3 getEntityPosAtNode(@NotNull Entity entity, int index) {
this.process();
return super.getEntityPosAtNode(entity, index);
}
@Override
public BlockPos getNodePos(int index) {
this.checkProcessed();
public @NotNull BlockPos getNodePos(int index) {
this.process();
return super.getNodePos(index);
}
@Override
public Vec3 getNextEntityPos(Entity entity) {
this.checkProcessed();
public @NotNull Vec3 getNextEntityPos(@NotNull Entity entity) {
this.process();
return super.getNextEntityPos(entity);
}
@Override
public BlockPos getNextNodePos() {
this.checkProcessed();
public @NotNull BlockPos getNextNodePos() {
this.process();
return super.getNextNodePos();
}
@Override
public Node getNextNode() {
this.checkProcessed();
public @NotNull Node getNextNode() {
this.process();
return super.getNextNode();
}
@Nullable
@Override
public Node getPreviousNode() {
this.checkProcessed();
public @Nullable Node getPreviousNode() {
this.process();
return super.getPreviousNode();
}
public PathProcessState getProcessState() {
return processState;
}
}

View File

@@ -69,9 +69,7 @@ public class AsyncPathProcessor {
*/
public static void awaitProcessing(@Nullable Path path, Consumer<@Nullable Path> afterProcessing) {
if (path != null && !path.isProcessed() && path instanceof AsyncPath asyncPath) {
asyncPath.postProcessing(() ->
MinecraftServer.getServer().scheduleOnMain(() -> afterProcessing.accept(path))
);
asyncPath.schedulePostProcessing(() -> afterProcessing.accept(path)); // Reduce double lambda allocation
} else {
afterProcessing.accept(path);
}

View File

@@ -1,7 +0,0 @@
package org.dreeam.leaf.async.path;
public enum PathProcessState {
WAITING,
PROCESSING,
COMPLETED,
}