9
0
mirror of https://github.com/Winds-Studio/Leaf.git synced 2025-12-25 18:09:17 +00:00

Fix build

This commit is contained in:
Dreeam
2025-03-31 16:33:30 -04:00
parent bbfb2815e4
commit ba43fad40e
25 changed files with 69 additions and 88 deletions

View File

@@ -0,0 +1,288 @@
package org.dreeam.leaf.async.path;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.pathfinder.Node;
import net.minecraft.world.level.pathfinder.Path;
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.function.Supplier;
/**
* 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
*/
private volatile PathProcessState processState = PathProcessState.WAITING;
/**
* runnables waiting for this to be processed
*/
private final List<Runnable> postProcessing = new ArrayList<>(0);
/**
* a list of positions that this path could path towards
*/
private final Set<BlockPos> positions;
/**
* the supplier of the real processed path
*/
private final Supplier<Path> pathSupplier;
/*
* Processed values
*/
/**
* 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
* <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
*/
private @Nullable BlockPos 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
*/
private float distToTarget = 0;
/**
* whether we can reach the target
* <p>
* while processing, we can always theoretically reach the target so default is true
*/
private boolean canReach = true;
public AsyncPath(@NotNull List<Node> emptyNodeList, @NotNull Set<BlockPos> positions, @NotNull Supplier<Path> pathSupplier) {
//noinspection ConstantConditions
super(emptyNodeList, null, false);
this.nodes = emptyNodeList;
this.positions = positions;
this.pathSupplier = pathSupplier;
AsyncPathProcessor.queue(this);
}
@Override
public boolean isProcessed() {
return this.processState == PathProcessState.COMPLETED;
}
/**
* returns the future representing the processing state of this path
*/
public synchronized void postProcessing(@NotNull Runnable runnable) {
if (isProcessed()) {
runnable.run();
} else {
this.postProcessing.add(runnable);
}
}
/**
* 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) {
if (this.positions.size() != positions.size()) {
return false;
}
return this.positions.containsAll(positions);
}
/**
* starts processing this path
*/
public synchronized void process() {
if (this.processState == PathProcessState.COMPLETED ||
this.processState == PathProcessState.PROCESSING) {
return;
}
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
}
/**
* 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();
}
}
/*
* overrides we need for final fields that we cannot modify after processing
*/
@Override
public @NotNull BlockPos getTarget() {
this.checkProcessed();
return this.target;
}
@Override
public float getDistToTarget() {
this.checkProcessed();
return this.distToTarget;
}
@Override
public boolean canReach() {
this.checkProcessed();
return this.canReach;
}
/*
* overrides to ensure we're processed first
*/
@Override
public boolean isDone() {
return this.processState == PathProcessState.COMPLETED && super.isDone();
}
@Override
public void advance() {
this.checkProcessed();
super.advance();
}
@Override
public boolean notStarted() {
this.checkProcessed();
return super.notStarted();
}
@Nullable
@Override
public Node getEndNode() {
this.checkProcessed();
return super.getEndNode();
}
@Override
public Node getNode(int index) {
this.checkProcessed();
return super.getNode(index);
}
@Override
public void truncateNodes(int length) {
this.checkProcessed();
super.truncateNodes(length);
}
@Override
public void replaceNode(int index, Node node) {
this.checkProcessed();
super.replaceNode(index, node);
}
@Override
public int getNodeCount() {
this.checkProcessed();
return super.getNodeCount();
}
@Override
public int getNextNodeIndex() {
this.checkProcessed();
return super.getNextNodeIndex();
}
@Override
public void setNextNodeIndex(int nodeIndex) {
this.checkProcessed();
super.setNextNodeIndex(nodeIndex);
}
@Override
public Vec3 getEntityPosAtNode(Entity entity, int index) {
this.checkProcessed();
return super.getEntityPosAtNode(entity, index);
}
@Override
public BlockPos getNodePos(int index) {
this.checkProcessed();
return super.getNodePos(index);
}
@Override
public Vec3 getNextEntityPos(Entity entity) {
this.checkProcessed();
return super.getNextEntityPos(entity);
}
@Override
public BlockPos getNextNodePos() {
this.checkProcessed();
return super.getNextNodePos();
}
@Override
public Node getNextNode() {
this.checkProcessed();
return super.getNextNode();
}
@Nullable
@Override
public Node getPreviousNode() {
this.checkProcessed();
return super.getPreviousNode();
}
public PathProcessState getProcessState() {
return processState;
}
}

View File

@@ -0,0 +1,100 @@
package org.dreeam.leaf.async.path;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.pathfinder.Path;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import java.util.function.Consumer;
/**
* used to handle the scheduling of async path processing
*/
public class AsyncPathProcessor {
private static final String THREAD_PREFIX = "Leaf Async Pathfinding";
private static final Logger LOGGER = LogManager.getLogger(THREAD_PREFIX);
private static long lastWarnMillis = System.currentTimeMillis();
private static final ThreadPoolExecutor pathProcessingExecutor = new ThreadPoolExecutor(
1,
org.dreeam.leaf.config.modules.async.AsyncPathfinding.asyncPathfindingMaxThreads,
org.dreeam.leaf.config.modules.async.AsyncPathfinding.asyncPathfindingKeepalive, TimeUnit.SECONDS,
getQueueImpl(),
new ThreadFactoryBuilder()
.setNameFormat(THREAD_PREFIX + " Thread - %d")
.setPriority(Thread.NORM_PRIORITY - 2)
.build(),
new RejectedTaskHandler()
);
private static class RejectedTaskHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable rejectedTask, ThreadPoolExecutor executor) {
BlockingQueue<Runnable> workQueue = executor.getQueue();
if (!executor.isShutdown()) {
switch (org.dreeam.leaf.config.modules.async.AsyncPathfinding.asyncPathfindingRejectPolicy) {
case FLUSH_ALL -> {
if (!workQueue.isEmpty()) {
List<Runnable> pendingTasks = new ArrayList<>(workQueue.size());
workQueue.drainTo(pendingTasks);
for (Runnable pendingTask : pendingTasks) {
pendingTask.run();
}
}
rejectedTask.run();
}
case CALLER_RUNS -> rejectedTask.run();
}
}
if (System.currentTimeMillis() - lastWarnMillis > 30000L) {
LOGGER.warn("Async pathfinding processor is busy! Pathfinding tasks will be treated as policy defined in config. Increasing max-threads in Leaf config may help.");
lastWarnMillis = System.currentTimeMillis();
}
}
}
protected static CompletableFuture<Void> queue(@NotNull AsyncPath path) {
return CompletableFuture.runAsync(path::process, pathProcessingExecutor)
.orTimeout(60L, TimeUnit.SECONDS)
.exceptionally(throwable -> {
if (throwable instanceof TimeoutException e) {
LOGGER.warn("Async Pathfinding process timed out", e);
} else LOGGER.warn("Error occurred while processing async path", throwable);
return null;
});
}
/**
* takes a possibly unprocessed path, and waits until it is completed
* the consumer will be immediately invoked if the path is already processed
* the consumer will always be called on the main thread
*
* @param path a path to wait on
* @param afterProcessing a consumer to be called
*/
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))
);
} else {
afterProcessing.accept(path);
}
}
private static BlockingQueue<Runnable> getQueueImpl() {
final int queueCapacity = org.dreeam.leaf.config.modules.async.AsyncPathfinding.asyncPathfindingQueueSize;
return new LinkedBlockingQueue<>(queueCapacity);
}}

View File

@@ -0,0 +1,46 @@
package org.dreeam.leaf.async.path;
import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
import net.minecraft.world.level.pathfinder.NodeEvaluator;
import org.apache.commons.lang.Validate;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
public class NodeEvaluatorCache {
private static final Map<NodeEvaluatorFeatures, MultiThreadedQueue<NodeEvaluator>> threadLocalNodeEvaluators = new ConcurrentHashMap<>();
private static final Map<NodeEvaluator, NodeEvaluatorGenerator> nodeEvaluatorToGenerator = new ConcurrentHashMap<>();
private static @NotNull Queue<NodeEvaluator> getQueueForFeatures(@NotNull NodeEvaluatorFeatures nodeEvaluatorFeatures) {
return threadLocalNodeEvaluators.computeIfAbsent(nodeEvaluatorFeatures, key -> new MultiThreadedQueue<>());
}
public static @NotNull NodeEvaluator takeNodeEvaluator(@NotNull NodeEvaluatorGenerator generator, @NotNull NodeEvaluator localNodeEvaluator) {
final NodeEvaluatorFeatures nodeEvaluatorFeatures = NodeEvaluatorFeatures.fromNodeEvaluator(localNodeEvaluator);
NodeEvaluator nodeEvaluator = getQueueForFeatures(nodeEvaluatorFeatures).poll();
if (nodeEvaluator == null) {
nodeEvaluator = generator.generate(nodeEvaluatorFeatures);
}
nodeEvaluatorToGenerator.put(nodeEvaluator, generator);
return nodeEvaluator;
}
public static void returnNodeEvaluator(@NotNull NodeEvaluator nodeEvaluator) {
final NodeEvaluatorGenerator generator = nodeEvaluatorToGenerator.remove(nodeEvaluator);
Validate.notNull(generator, "NodeEvaluator already returned");
final NodeEvaluatorFeatures nodeEvaluatorFeatures = NodeEvaluatorFeatures.fromNodeEvaluator(nodeEvaluator);
getQueueForFeatures(nodeEvaluatorFeatures).offer(nodeEvaluator);
}
public static void removeNodeEvaluator(@NotNull NodeEvaluator nodeEvaluator) {
nodeEvaluatorToGenerator.remove(nodeEvaluator);
}
}

View File

@@ -0,0 +1,23 @@
package org.dreeam.leaf.async.path;
import net.minecraft.world.level.pathfinder.NodeEvaluator;
import net.minecraft.world.level.pathfinder.SwimNodeEvaluator;
public record NodeEvaluatorFeatures(
NodeEvaluatorType type,
boolean canPassDoors,
boolean canFloat,
boolean canWalkOverFences,
boolean canOpenDoors,
boolean allowBreaching
) {
public static NodeEvaluatorFeatures fromNodeEvaluator(NodeEvaluator nodeEvaluator) {
NodeEvaluatorType type = NodeEvaluatorType.fromNodeEvaluator(nodeEvaluator);
boolean canPassDoors = nodeEvaluator.canPassDoors();
boolean canFloat = nodeEvaluator.canFloat();
boolean canWalkOverFences = nodeEvaluator.canWalkOverFences();
boolean canOpenDoors = nodeEvaluator.canOpenDoors();
boolean allowBreaching = nodeEvaluator instanceof SwimNodeEvaluator swimNodeEvaluator && swimNodeEvaluator.allowBreaching;
return new NodeEvaluatorFeatures(type, canPassDoors, canFloat, canWalkOverFences, canOpenDoors, allowBreaching);
}
}

View File

@@ -0,0 +1,8 @@
package org.dreeam.leaf.async.path;
import net.minecraft.world.level.pathfinder.NodeEvaluator;
import org.jetbrains.annotations.NotNull;
public interface NodeEvaluatorGenerator {
@NotNull NodeEvaluator generate(NodeEvaluatorFeatures nodeEvaluatorFeatures);
}

View File

@@ -0,0 +1,17 @@
package org.dreeam.leaf.async.path;
import net.minecraft.world.level.pathfinder.*;
public enum NodeEvaluatorType {
WALK,
SWIM,
AMPHIBIOUS,
FLY;
public static NodeEvaluatorType fromNodeEvaluator(NodeEvaluator nodeEvaluator) {
if (nodeEvaluator instanceof SwimNodeEvaluator) return SWIM;
if (nodeEvaluator instanceof FlyNodeEvaluator) return FLY;
if (nodeEvaluator instanceof AmphibiousNodeEvaluator) return AMPHIBIOUS;
return WALK;
}
}

View File

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

View File

@@ -0,0 +1,19 @@
package org.dreeam.leaf.async.path;
import org.dreeam.leaf.config.LeafConfig;
import java.util.Locale;
public enum PathfindTaskRejectPolicy {
FLUSH_ALL,
CALLER_RUNS;
public static PathfindTaskRejectPolicy fromString(String policy) {
try {
return PathfindTaskRejectPolicy.valueOf(policy.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
LeafConfig.LOGGER.warn("Invalid pathfind task reject policy: {}, falling back to {}.", policy, FLUSH_ALL.toString());
return FLUSH_ALL;
}
}
}

View File

@@ -0,0 +1,51 @@
package org.dreeam.leaf.config.modules.async;
import org.dreeam.leaf.async.path.PathfindTaskRejectPolicy;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
import org.dreeam.leaf.config.LeafConfig;
public class AsyncPathfinding extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.ASYNC.getBaseKeyName() + ".async-pathfinding";
}
public static boolean enabled = false;
public static int asyncPathfindingMaxThreads = 0;
public static int asyncPathfindingKeepalive = 60;
public static int asyncPathfindingQueueSize = 0;
public static PathfindTaskRejectPolicy asyncPathfindingRejectPolicy = PathfindTaskRejectPolicy.FLUSH_ALL;
@Override
public void onLoaded() {
final int availableProcessors = Runtime.getRuntime().availableProcessors();
enabled = config.getBoolean(getBasePath() + ".enabled", enabled);
asyncPathfindingMaxThreads = config.getInt(getBasePath() + ".max-threads", asyncPathfindingMaxThreads);
asyncPathfindingKeepalive = config.getInt(getBasePath() + ".keepalive", asyncPathfindingKeepalive);
asyncPathfindingQueueSize = config.getInt(getBasePath() + ".queue-size", asyncPathfindingQueueSize);
if (asyncPathfindingMaxThreads < 0)
asyncPathfindingMaxThreads = Math.max(availableProcessors + asyncPathfindingMaxThreads, 1);
else if (asyncPathfindingMaxThreads == 0)
asyncPathfindingMaxThreads = Math.max(availableProcessors / 4, 1);
if (!enabled)
asyncPathfindingMaxThreads = 0;
else
LeafConfig.LOGGER.info("Using {} threads for Async Pathfinding", asyncPathfindingMaxThreads);
if (asyncPathfindingQueueSize <= 0)
asyncPathfindingQueueSize = asyncPathfindingMaxThreads * 256;
asyncPathfindingRejectPolicy = PathfindTaskRejectPolicy.fromString(config.getString(getBasePath() + ".reject-policy", availableProcessors >= 12 && asyncPathfindingQueueSize < 512 ? PathfindTaskRejectPolicy.FLUSH_ALL.toString() : PathfindTaskRejectPolicy.CALLER_RUNS.toString(), config.pickStringRegionBased(
"""
The policy to use when the queue is full and a new task is submitted.
FLUSH_ALL: All pending tasks will be run on server thread.
CALLER_RUNS: Newly submitted task will be run on server thread.""",
"""
当队列满时, 新提交的任务将使用以下策略处理.
FLUSH_ALL: 所有等待中的任务都将在主线程上运行.
CALLER_RUNS: 新提交的任务将在主线程上运行."""
)));
}
}