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 9afa9c8d..44f554e6 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 @@ -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 postProcessing = new ArrayList<>(0); + private final ConcurrentLinkedQueue 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 positions; /** - * the supplier of the real processed path + * The supplier of the real processed path */ private final Supplier 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 nodes; /** - * the block we're trying to path to + * The block we're trying to path to *

- * 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 *

- * 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 *

- * 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 emptyNodeList, @NotNull Set positions, @NotNull Supplier 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 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); } /** - * 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; - } } 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 a3ab0bac..b1764fb1 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 @@ -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); } diff --git a/leaf-server/src/main/java/org/dreeam/leaf/async/path/PathProcessState.java b/leaf-server/src/main/java/org/dreeam/leaf/async/path/PathProcessState.java deleted file mode 100644 index 73f30b73..00000000 --- a/leaf-server/src/main/java/org/dreeam/leaf/async/path/PathProcessState.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.dreeam.leaf.async.path; - -public enum PathProcessState { - WAITING, - PROCESSING, - COMPLETED, -}