9
0
mirror of https://github.com/BX-Team/DivineMC.git synced 2025-12-22 08:19:19 +00:00

add back async pathfinding feature

This commit is contained in:
NONPLAYT
2025-01-15 18:47:48 +03:00
parent 0b3c4ab337
commit 6637492513
12 changed files with 1310 additions and 1338 deletions

View File

@@ -2,8 +2,12 @@ package space.bxteam.divinemc.configuration;
import io.papermc.paper.configuration.Configuration;
import io.papermc.paper.configuration.ConfigurationPart;
import org.bukkit.Bukkit;
import org.spongepowered.configurate.objectmapping.meta.PostProcess;
import org.spongepowered.configurate.objectmapping.meta.Setting;
import java.util.logging.Level;
@SuppressWarnings({"CanBeFinal", "FieldCanBeLocal", "FieldMayBeFinal", "NotNullFieldNotInitialized", "InnerClassMayBeStatic"})
public class DivineGlobalConfiguration extends ConfigurationPart {
static final int CURRENT_VERSION = 4;
@@ -21,6 +25,29 @@ public class DivineGlobalConfiguration extends ConfigurationPart {
@Setting(Configuration.VERSION_FIELD)
public int version = CURRENT_VERSION;
public AsyncPathfinding asyncPathfinding;
public class AsyncPathfinding extends ConfigurationPart {
public boolean asyncPathfinding = true;
public int asyncPathfindingMaxThreads = 0;
public int asyncPathfindingKeepalive = 60;
@PostProcess
public void init() {
if (asyncPathfindingMaxThreads < 0) {
asyncPathfindingMaxThreads = Math.max(Runtime.getRuntime().availableProcessors() + asyncPathfindingMaxThreads, 1);
} else if (asyncPathfindingMaxThreads == 0) {
asyncPathfindingMaxThreads = Math.max(Runtime.getRuntime().availableProcessors() / 4, 1);
}
if (!asyncPathfinding) {
asyncPathfindingMaxThreads = 0;
} else {
Bukkit.getLogger().log(Level.INFO, "Using " + asyncPathfindingMaxThreads + " threads for Async Pathfinding");
}
}
}
public Chat chat;
public class Chat extends ConfigurationPart {

View File

@@ -0,0 +1,284 @@
package space.bxteam.divinemc.pathfinding;
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;
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,48 @@
package space.bxteam.divinemc.pathfinding;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.pathfinder.Path;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.concurrent.*;
import java.util.function.Consumer;
/**
* used to handle the scheduling of async path processing
*/
public class AsyncPathProcessor {
private static final Executor pathProcessingExecutor = new ThreadPoolExecutor(
1,
space.bxteam.divinemc.configuration.DivineGlobalConfiguration.get().asyncPathfinding.asyncPathfindingMaxThreads,
space.bxteam.divinemc.configuration.DivineGlobalConfiguration.get().asyncPathfinding.asyncPathfindingKeepalive, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
new ThreadFactoryBuilder()
.setNameFormat("DivineMC Async Pathfinding Thread - %d")
.setPriority(Thread.NORM_PRIORITY - 2)
.build()
);
protected static CompletableFuture<Void> queue(@NotNull AsyncPath path) {
return CompletableFuture.runAsync(path::process, pathProcessingExecutor);
}
/**
* 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);
}
}
}

View File

@@ -0,0 +1,44 @@
package space.bxteam.divinemc.pathfinding;
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;
import java.util.concurrent.ConcurrentLinkedQueue;
public class NodeEvaluatorCache {
private static final Map<NodeEvaluatorFeatures, ConcurrentLinkedQueue<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 ConcurrentLinkedQueue<>());
}
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 space.bxteam.divinemc.pathfinding;
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 space.bxteam.divinemc.pathfinding;
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 space.bxteam.divinemc.pathfinding;
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 space.bxteam.divinemc.pathfinding;
public enum PathProcessState {
WAITING,
PROCESSING,
COMPLETED
}