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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package org.dreeam.leaf.async.path;
|
||||
|
||||
public enum PathProcessState {
|
||||
WAITING,
|
||||
PROCESSING,
|
||||
COMPLETED,
|
||||
}
|
||||
Reference in New Issue
Block a user