diff --git a/README.md b/README.md
index 5c33c66..38c62a4 100644
--- a/README.md
+++ b/README.md
@@ -82,7 +82,13 @@ current route until the replacement lands, so a chased target never stands
still.
`controller.state()` reports progress: `COMPLETED` arrived, `PARTIAL` ran out
-short of the goal, `STUCK` cannot walk the route it has.
+short of the goal, `STUCK` cannot walk the route it has. Chunks that are not
+loaded count as solid, so a mob heading into them stops at the edge with
+`controller.routeStop() == PathStop.TERRAIN_UNLOADED`; call `moveTo` again once
+they load.
+
+Need a fixed profile, another speed, or direct movement? Use
+`navigation.controllerBuilder(mob)`.
That is the whole everyday API; see the [wiki](https://github.com/AtlasEngineCa/Pathfinding/wiki).
diff --git a/build.gradle.kts b/build.gradle.kts
index 09eaf7c..e9dc7fe 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -20,7 +20,7 @@ jmh {
}
group = "ca.atlasengine"
-version = "10.3.0"
+version = "11.0.0"
repositories {
mavenCentral()
@@ -73,6 +73,12 @@ tasks.withType().configureEach {
options.release = 25
}
+// The module exports only its API packages. A public signature naming a type
+// from a non-exported package compiles here but not for a modular consumer.
+tasks.named("compileJava") {
+ options.compilerArgs.addAll(listOf("-Xlint:exports", "-Werror"))
+}
+
// Runnable demo server. It compiles against the published packages only, so
// `build` failing here means the public API moved under an integration.
val examples = sourceSets.create("examples")
diff --git a/src/examples/java/ca/atlasengine/pathfinding/examples/DemoMob.java b/src/examples/java/ca/atlasengine/pathfinding/examples/DemoMob.java
index 800fa9d..65b7d33 100644
--- a/src/examples/java/ca/atlasengine/pathfinding/examples/DemoMob.java
+++ b/src/examples/java/ca/atlasengine/pathfinding/examples/DemoMob.java
@@ -1,9 +1,9 @@
package ca.atlasengine.pathfinding.examples;
-import net.kyori.adventure.text.Component;
import ca.atlasengine.pathfinding.EntityNavigationController;
import ca.atlasengine.pathfinding.NavigationState;
import ca.atlasengine.pathfinding.PathDebugRenderer;
+import net.kyori.adventure.text.Component;
import net.minestom.server.component.DataComponents;
import net.minestom.server.coordinate.Point;
import net.minestom.server.entity.EntityCreature;
diff --git a/src/examples/java/ca/atlasengine/pathfinding/examples/DemoSession.java b/src/examples/java/ca/atlasengine/pathfinding/examples/DemoSession.java
index d068ccd..4327b41 100644
--- a/src/examples/java/ca/atlasengine/pathfinding/examples/DemoSession.java
+++ b/src/examples/java/ca/atlasengine/pathfinding/examples/DemoSession.java
@@ -1,7 +1,7 @@
package ca.atlasengine.pathfinding.examples;
-import ca.atlasengine.pathfinding.profile.NavigationProfile;
import ca.atlasengine.pathfinding.NavigationSystem;
+import ca.atlasengine.pathfinding.profile.NavigationProfile;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityType;
@@ -42,7 +42,8 @@ public NavigationSystem navigation() {
public DemoMob spawn(EntityType type, String label, Pos at) {
DemoMob mob = new DemoMob(type, label);
mob.setInstance(instance, at).join();
- return track(mob.navigateWith(navigation.controller(mob, SPEED)));
+ return track(mob.navigateWith(navigation.controllerBuilder(mob)
+ .movementPerTick(SPEED).build()));
}
/** Spawns a mob navigating with an explicitly composed profile. */
@@ -51,7 +52,8 @@ public DemoMob spawn(EntityType type, String label, Pos at,
DemoMob mob = new DemoMob(type, label);
mob.setInstance(instance, at).join();
return track(mob.navigateWith(
- navigation.controller(mob, profile, SPEED)));
+ navigation.controllerBuilder(mob).profile(profile)
+ .movementPerTick(SPEED).build()));
}
/** Spawns a mob that pursues {@code quarry} through the shared mesh. */
diff --git a/src/examples/java/ca/atlasengine/pathfinding/examples/Demos.java b/src/examples/java/ca/atlasengine/pathfinding/examples/Demos.java
index 303259e..3e2e1cb 100644
--- a/src/examples/java/ca/atlasengine/pathfinding/examples/Demos.java
+++ b/src/examples/java/ca/atlasengine/pathfinding/examples/Demos.java
@@ -1,26 +1,26 @@
package ca.atlasengine.pathfinding.examples;
-import net.kyori.adventure.text.Component;
-import net.kyori.adventure.text.format.NamedTextColor;
+import ca.atlasengine.pathfinding.EntityTargets;
+import ca.atlasengine.pathfinding.NavigationSystem;
+import ca.atlasengine.pathfinding.adaptive.SharedMeshStatus;
+import ca.atlasengine.pathfinding.influence.NavigationInfluence;
+import ca.atlasengine.pathfinding.influence.NavigationZoneInfluence;
+import ca.atlasengine.pathfinding.metrics.NavigationMetricsSnapshot;
import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;
import ca.atlasengine.pathfinding.profile.ClimbableCapabilities;
import ca.atlasengine.pathfinding.profile.GroundCapabilities;
-import ca.atlasengine.pathfinding.metrics.NavigationMetricsSnapshot;
import ca.atlasengine.pathfinding.profile.NavigationMode;
import ca.atlasengine.pathfinding.profile.NavigationModifiers;
import ca.atlasengine.pathfinding.profile.NavigationProfile;
-import ca.atlasengine.pathfinding.search.NavigationRequest;
-import ca.atlasengine.pathfinding.NavigationSystem;
-import ca.atlasengine.pathfinding.internal.EntityTargets;
-import ca.atlasengine.pathfinding.result.PathResult;
import ca.atlasengine.pathfinding.profile.PlatformJumpCapabilities;
-import ca.atlasengine.pathfinding.adaptive.SharedMeshStatus;
-import ca.atlasengine.pathfinding.influence.NavigationInfluence;
-import ca.atlasengine.pathfinding.influence.NavigationZoneInfluence;
+import ca.atlasengine.pathfinding.result.PathResult;
+import ca.atlasengine.pathfinding.search.NavigationRequest;
import ca.atlasengine.pathfinding.terrain.BlockManipulationCapabilities;
import ca.atlasengine.pathfinding.terrain.MobTraversalProfile;
import ca.atlasengine.pathfinding.terrain.TerrainClassification;
import ca.atlasengine.pathfinding.terrain.TerrainType;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
import net.minestom.server.collision.BoundingBox;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.EntityType;
@@ -165,7 +165,8 @@ public static void custom(DemoSession session) {
GroundCapabilities body = GroundCapabilities.STANDARD
.withPlatformJump(PlatformJumpCapabilities.acrossGaps(1));
- NavigationProfile profile = NavigationProfile.builder(NavigationMode.GROUND, courier, body).allowBreaching(false).prefersShallowWater(// allowBreaching
+ NavigationProfile profile = NavigationProfile.builder(NavigationMode.GROUND, courier,
+ body).prefersShallowWater(// allowBreaching
false).avoidSun(// prefersShallowWater
false).build(); // avoidSun
diff --git a/src/examples/java/ca/atlasengine/pathfinding/examples/DocumentedExamples.java b/src/examples/java/ca/atlasengine/pathfinding/examples/DocumentedExamples.java
index fd97585..21decdc 100644
--- a/src/examples/java/ca/atlasengine/pathfinding/examples/DocumentedExamples.java
+++ b/src/examples/java/ca/atlasengine/pathfinding/examples/DocumentedExamples.java
@@ -1,25 +1,18 @@
package ca.atlasengine.pathfinding.examples;
-import net.minestom.server.instance.block.Block;
-import ca.atlasengine.pathfinding.profile.ClimbableCapabilities;
import ca.atlasengine.pathfinding.EntityNavigationController;
-import ca.atlasengine.pathfinding.profile.GroundCapabilities;
-import ca.atlasengine.pathfinding.profile.NavigationMode;
-import ca.atlasengine.pathfinding.profile.NavigationModifiers;
import ca.atlasengine.pathfinding.NavigationOptions;
-import ca.atlasengine.pathfinding.profile.NavigationProfile;
import ca.atlasengine.pathfinding.NavigationState;
import ca.atlasengine.pathfinding.NavigationSystem;
import ca.atlasengine.pathfinding.PathDebugRenderer;
-import ca.atlasengine.pathfinding.event.NavigationCompletedEvent;
-import ca.atlasengine.pathfinding.event.NavigationStuckEvent;
-import ca.atlasengine.pathfinding.event.RouteReplanEvent;
-import ca.atlasengine.pathfinding.profile.PlatformJumpCapabilities;
import ca.atlasengine.pathfinding.adaptive.SharedMeshNavigation;
import ca.atlasengine.pathfinding.adaptive.SharedMeshOptions;
import ca.atlasengine.pathfinding.adaptive.SharedMeshPolicy;
import ca.atlasengine.pathfinding.adaptive.SharedMeshPursuit;
import ca.atlasengine.pathfinding.adaptive.SharedMeshStatus;
+import ca.atlasengine.pathfinding.event.NavigationCompletedEvent;
+import ca.atlasengine.pathfinding.event.NavigationStuckEvent;
+import ca.atlasengine.pathfinding.event.RouteReplanEvent;
import ca.atlasengine.pathfinding.influence.BlockAvoidanceInfluence;
import ca.atlasengine.pathfinding.influence.EntityFearInfluence;
import ca.atlasengine.pathfinding.influence.EntitySnapshot;
@@ -27,6 +20,12 @@
import ca.atlasengine.pathfinding.influence.NavigationInfluence;
import ca.atlasengine.pathfinding.influence.NavigationZoneInfluence;
import ca.atlasengine.pathfinding.influence.ReturnRadiusInfluence;
+import ca.atlasengine.pathfinding.profile.ClimbableCapabilities;
+import ca.atlasengine.pathfinding.profile.GroundCapabilities;
+import ca.atlasengine.pathfinding.profile.NavigationMode;
+import ca.atlasengine.pathfinding.profile.NavigationModifiers;
+import ca.atlasengine.pathfinding.profile.NavigationProfile;
+import ca.atlasengine.pathfinding.profile.PlatformJumpCapabilities;
import ca.atlasengine.pathfinding.terrain.BlockManipulationCapabilities;
import ca.atlasengine.pathfinding.terrain.MobTraversalProfile;
import ca.atlasengine.pathfinding.terrain.TerrainClassification;
@@ -34,6 +33,7 @@
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.Entity;
+import net.minestom.server.instance.block.Block;
import java.util.List;
import java.util.Map;
@@ -144,7 +144,8 @@ static EntityNavigationController customMob(
.avoidSun(true)
.build();
- return navigation.controller(entity, profile, 0.2);
+ return navigation.controllerBuilder(entity).profile(profile)
+ .movementPerTick(0.2).build();
}
/** Wiki, "Platform jumping", "Planned climbing", "Doors". */
diff --git a/src/examples/java/ca/atlasengine/pathfinding/examples/NavCommand.java b/src/examples/java/ca/atlasengine/pathfinding/examples/NavCommand.java
index d4f5c82..b138937 100644
--- a/src/examples/java/ca/atlasengine/pathfinding/examples/NavCommand.java
+++ b/src/examples/java/ca/atlasengine/pathfinding/examples/NavCommand.java
@@ -1,10 +1,11 @@
package ca.atlasengine.pathfinding.examples;
-import ca.atlasengine.pathfinding.profile.ClimbableCapabilities;import ca.atlasengine.pathfinding.profile.PlatformJumpCapabilities;import net.kyori.adventure.text.Component;
+import ca.atlasengine.pathfinding.profile.ClimbableCapabilities;
+import ca.atlasengine.pathfinding.profile.PlatformJumpCapabilities;
+import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.Command;
-import net.minestom.server.command.builder.CommandContext;
import net.minestom.server.command.builder.arguments.ArgumentType;
import net.minestom.server.entity.Player;
diff --git a/src/examples/java/ca/atlasengine/pathfinding/examples/NavigationDemoServer.java b/src/examples/java/ca/atlasengine/pathfinding/examples/NavigationDemoServer.java
index 26c03b5..f3f3fe9 100644
--- a/src/examples/java/ca/atlasengine/pathfinding/examples/NavigationDemoServer.java
+++ b/src/examples/java/ca/atlasengine/pathfinding/examples/NavigationDemoServer.java
@@ -1,9 +1,9 @@
package ca.atlasengine.pathfinding.examples;
-import net.kyori.adventure.text.Component;
-import net.kyori.adventure.text.format.NamedTextColor;
import ca.atlasengine.pathfinding.NavigationSystem;
import ca.atlasengine.pathfinding.adaptive.SharedMeshOptions;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
import net.minestom.server.Auth;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.GameMode;
diff --git a/src/jmh/java/ca/atlasengine/pathfinding/AdaptiveNavigationBenchmark.java b/src/jmh/java/ca/atlasengine/pathfinding/AdaptiveNavigationBenchmark.java
index 03456a7..d24bbb4 100644
--- a/src/jmh/java/ca/atlasengine/pathfinding/AdaptiveNavigationBenchmark.java
+++ b/src/jmh/java/ca/atlasengine/pathfinding/AdaptiveNavigationBenchmark.java
@@ -1,10 +1,16 @@
package ca.atlasengine.pathfinding;
-import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.profile.NavigationProfile;import ca.atlasengine.pathfinding.result.PathNode;import ca.atlasengine.pathfinding.result.PathStatus;import ca.atlasengine.pathfinding.search.NavigationRequest;import ca.atlasengine.pathfinding.adaptive.SharedMeshPolicy;
-import ca.atlasengine.pathfinding.internal.adaptive.SharedMeshCoordinator;
+import ca.atlasengine.pathfinding.NavigationPlan;
import ca.atlasengine.pathfinding.adaptive.SharedMeshHandle;
+import ca.atlasengine.pathfinding.adaptive.SharedMeshPolicy;
import ca.atlasengine.pathfinding.adaptive.SharedMeshSource;
-import ca.atlasengine.pathfinding.NavigationPlan;
+import ca.atlasengine.pathfinding.internal.adaptive.SharedMeshCoordinator;
+import ca.atlasengine.pathfinding.metrics.NavigationMetrics;
+import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;
+import ca.atlasengine.pathfinding.profile.NavigationProfile;
+import ca.atlasengine.pathfinding.result.PathNode;
+import ca.atlasengine.pathfinding.result.PathStatus;
+import ca.atlasengine.pathfinding.search.NavigationRequest;
import net.minestom.server.collision.BoundingBox;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.EntityType;
@@ -77,9 +83,13 @@ public void setup() {
.promotionRequests(2).retentionRequests(1)
.maximumNodesPerRegion(1_024).build();
unpromoted = new SharedMeshCoordinator(
- (actor, ignored) -> CompletableFuture.completedFuture(plan), cold);
+ (actor,
+ ignored) -> CompletableFuture.completedFuture(plan), cold,
+ new NavigationMetrics());
promoted = new SharedMeshCoordinator(
- (actor, ignored) -> CompletableFuture.completedFuture(plan), hot);
+ (actor,
+ ignored) -> CompletableFuture.completedFuture(plan), hot,
+ new NavigationMetrics());
promoted.request("seed-1", "target", 1, 0, request).plan().join();
promoted.request("seed-2", "target", 1, 1, request).plan().join();
if (promoted.stats().promotedRegions() != 1) {
@@ -95,7 +105,7 @@ private NavigationPlan straightPlan() {
}
return new NavigationPlan(
request.start(), request.target(), box, profile,
- PathStatus.FOUND, nodes, nodes.size(), nodes.size() * 8);
+ PathStatus.FOUND, nodes, nodes.size(), nodes.size() * 8, List.of());
}
}
}
diff --git a/src/jmh/java/ca/atlasengine/pathfinding/PathfindingBenchmark.java b/src/jmh/java/ca/atlasengine/pathfinding/PathfindingBenchmark.java
index aa494dd..dc6ab00 100644
--- a/src/jmh/java/ca/atlasengine/pathfinding/PathfindingBenchmark.java
+++ b/src/jmh/java/ca/atlasengine/pathfinding/PathfindingBenchmark.java
@@ -1,9 +1,20 @@
package ca.atlasengine.pathfinding;
-import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.profile.ClimbableCapabilities;import ca.atlasengine.pathfinding.profile.GroundCapabilities;import ca.atlasengine.pathfinding.profile.NavigationMode;import ca.atlasengine.pathfinding.profile.NavigationProfile;import ca.atlasengine.pathfinding.result.PathNode;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.search.DiscreteGroundPathfinder;import ca.atlasengine.pathfinding.search.EntityPathfinder;import ca.atlasengine.pathfinding.search.GroundSearchLimits;import ca.atlasengine.pathfinding.search.NavigationRequest;import ca.atlasengine.pathfinding.search.SearchControl;import ca.atlasengine.pathfinding.terrain.MobTraversalProfile;
+import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;
+import ca.atlasengine.pathfinding.profile.ClimbableCapabilities;
+import ca.atlasengine.pathfinding.profile.GroundCapabilities;
+import ca.atlasengine.pathfinding.profile.NavigationMode;
+import ca.atlasengine.pathfinding.profile.NavigationProfile;
+import ca.atlasengine.pathfinding.result.PathNode;
+import ca.atlasengine.pathfinding.result.PathResult;
+import ca.atlasengine.pathfinding.search.EntityPathfinder;
+import ca.atlasengine.pathfinding.search.NavigationRequest;
+import ca.atlasengine.pathfinding.search.SearchControl;
+import ca.atlasengine.pathfinding.terrain.MobTraversalProfile;
import net.minestom.server.collision.BoundingBox;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
+import net.minestom.server.entity.EntityType;
import net.minestom.server.instance.block.Block;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
@@ -127,10 +138,27 @@ private static int checksum(PathResult result) {
return checksum;
}
+ /**
+ * A default-mob ground search between two exact cells, with the budget
+ * of eight expansions per node of path length used throughout.
+ */
+ static NavigationRequest groundRequest(World world, Pos start, Pos target,
+ BoundingBox box, double maxLength) {
+ NavigationProfile ground = NavigationProfile.builder(
+ NavigationMode.GROUND, MobTraversalProfile.DEFAULT,
+ GroundCapabilities.STANDARD)
+ .pathToTargetsBelowSurface(true)
+ .build();
+ return NavigationRequest.builder(world, start, target, box, ground)
+ .maxPathLength(maxLength)
+ .maxVisitedMultiplier(8)
+ .build();
+ }
+
@State(Scope.Benchmark)
public static class OpenGround {
final World world = World.flat(-4, 516, -4, 4);
- final DiscreteGroundPathfinder engine = new DiscreteGroundPathfinder();
+ final EntityPathfinder engine = new EntityPathfinder();
final Pos start = new Pos(0.5, 1, 0.5);
final Pos target = new Pos(512.5, 1, 0.5);
final Vec targetVector = target.asVec();
@@ -138,7 +166,9 @@ public static class OpenGround {
int expectedChecksum;
PathResult search() {
- return engine.findPath(world, start, target, box, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(520).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.STANDARD, SearchControl.NONE);
+ return engine.findPath(
+ groundRequest(world, start, target, box, 520),
+ SearchControl.NONE);
}
@Setup(Level.Trial)
@@ -155,7 +185,7 @@ public void setup() {
@State(Scope.Benchmark)
public static class AlternatingWalls {
final World world = World.flat(-4, 260, -24, 24);
- final DiscreteGroundPathfinder engine = new DiscreteGroundPathfinder();
+ final EntityPathfinder engine = new EntityPathfinder();
final Pos start = new Pos(0.5, 1, 0.5);
final Pos targetPosition = new Pos(256.5, 1, 0.5);
final Vec target = targetPosition.asVec();
@@ -164,7 +194,9 @@ public static class AlternatingWalls {
int expectedChecksum;
PathResult search() {
- return engine.findPath(world, start, targetPosition, box, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(400).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.STANDARD, SearchControl.NONE);
+ return engine.findPath(
+ groundRequest(world, start, targetPosition, box, 400),
+ SearchControl.NONE);
}
@Setup(Level.Trial)
@@ -209,7 +241,8 @@ public void setup() {
}
}
}
- NavigationProfile profile = NavigationProfile.builder(NavigationMode.GROUND, MobTraversalProfile.DEFAULT, GroundCapabilities.STANDARD).allowBreaching(false).prefersShallowWater(false).avoidSun(false).build();
+ NavigationProfile profile = NavigationProfile.builder(NavigationMode.GROUND,
+ MobTraversalProfile.DEFAULT, GroundCapabilities.STANDARD).build();
request = NavigationRequest.builder(
world, start, targetPosition, box, profile)
.maxPathLength(400)
@@ -239,7 +272,8 @@ public void setup() {
for (int z = -3; z <= 3; z++) world.set(x, y, z, Block.STONE);
}
}
- NavigationProfile profile = NavigationProfile.builder(NavigationMode.FLYING, MobTraversalProfile.DEFAULT, GroundCapabilities.STANDARD).allowBreaching(false).prefersShallowWater(false).avoidSun(false).build();
+ NavigationProfile profile = NavigationProfile.builder(NavigationMode.FLYING,
+ MobTraversalProfile.DEFAULT, GroundCapabilities.STANDARD).build();
request = NavigationRequest.builder(world,
new Vec(0.5, 0.5, 0.5), target,
new BoundingBox(0.6, 0.6, 0.6), profile)
@@ -269,9 +303,10 @@ public ClimbableTower() {
}
NavigationProfile base = BuiltinNavigationProfiles
.forEntityType(
- net.minestom.server.entity.EntityType.ZOMBIE);
- NavigationProfile climbing = NavigationProfile.builder(base.mode(), base.mobProfile(), base.groundCapabilities().withClimbables(
- ClimbableCapabilities.STANDARD)).allowBreaching(base.allowBreaching()).prefersShallowWater(base.prefersShallowWater()).avoidSun(base.avoidSun()).directPathIgnoresFluids(base.directPathIgnoresFluids()).pathToTargetsBelowSurface(base.pathToTargetsBelowSurface()).build();
+ EntityType.ZOMBIE);
+ NavigationProfile climbing = base.withGroundCapabilities(
+ base.groundCapabilities().withClimbables(
+ ClimbableCapabilities.STANDARD));
request = NavigationRequest.builder(world,
new Pos(0.5, 1, 0.5),
new Pos(1.5, 33, 0.5),
diff --git a/src/jmh/java/ca/atlasengine/pathfinding/PlatformJumpBenchmark.java b/src/jmh/java/ca/atlasengine/pathfinding/PlatformJumpBenchmark.java
index 6645b46..5ff13b8 100644
--- a/src/jmh/java/ca/atlasengine/pathfinding/PlatformJumpBenchmark.java
+++ b/src/jmh/java/ca/atlasengine/pathfinding/PlatformJumpBenchmark.java
@@ -1,6 +1,15 @@
package ca.atlasengine.pathfinding;
-import ca.atlasengine.pathfinding.profile.GroundCapabilities;import ca.atlasengine.pathfinding.profile.NavigationMode;import ca.atlasengine.pathfinding.profile.NavigationProfile;import ca.atlasengine.pathfinding.profile.PlatformJumpCapabilities;import ca.atlasengine.pathfinding.result.PathNode;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.search.EntityPathfinder;import ca.atlasengine.pathfinding.search.NavigationRequest;import ca.atlasengine.pathfinding.search.SearchControl;import ca.atlasengine.pathfinding.terrain.MobTraversalProfile;
+import ca.atlasengine.pathfinding.profile.GroundCapabilities;
+import ca.atlasengine.pathfinding.profile.NavigationMode;
+import ca.atlasengine.pathfinding.profile.NavigationProfile;
+import ca.atlasengine.pathfinding.profile.PlatformJumpCapabilities;
+import ca.atlasengine.pathfinding.result.PathNode;
+import ca.atlasengine.pathfinding.result.PathResult;
+import ca.atlasengine.pathfinding.search.EntityPathfinder;
+import ca.atlasengine.pathfinding.search.NavigationRequest;
+import ca.atlasengine.pathfinding.search.SearchControl;
+import ca.atlasengine.pathfinding.terrain.MobTraversalProfile;
import net.minestom.server.collision.BoundingBox;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.instance.block.Block;
@@ -57,7 +66,8 @@ private static int checksum(PathResult result) {
}
private static NavigationProfile jumping(PlatformJumpCapabilities jump) {
- return NavigationProfile.builder(NavigationMode.GROUND, MobTraversalProfile.DEFAULT, GroundCapabilities.STANDARD.withPlatformJump(jump)).allowBreaching(false).prefersShallowWater(false).avoidSun(false).build();
+ return NavigationProfile.builder(NavigationMode.GROUND, MobTraversalProfile.DEFAULT,
+ GroundCapabilities.STANDARD.withPlatformJump(jump)).build();
}
/** Alternating one-block islands: every edge of the route is an arc. */
diff --git a/src/jmh/java/ca/atlasengine/pathfinding/adaptive/SharedNavigationBenchmark.java b/src/jmh/java/ca/atlasengine/pathfinding/adaptive/SharedNavigationBenchmark.java
index 5602457..d78f5b1 100644
--- a/src/jmh/java/ca/atlasengine/pathfinding/adaptive/SharedNavigationBenchmark.java
+++ b/src/jmh/java/ca/atlasengine/pathfinding/adaptive/SharedNavigationBenchmark.java
@@ -1,14 +1,14 @@
package ca.atlasengine.pathfinding.adaptive;
import ca.atlasengine.pathfinding.NavigationPlan;
-import ca.atlasengine.pathfinding.search.EntityPathfinder;
+import ca.atlasengine.pathfinding.PathfindingBenchmark;
import ca.atlasengine.pathfinding.profile.GroundCapabilities;
import ca.atlasengine.pathfinding.profile.NavigationMode;
import ca.atlasengine.pathfinding.profile.NavigationProfile;
-import ca.atlasengine.pathfinding.search.NavigationRequest;
import ca.atlasengine.pathfinding.result.PathNode;
import ca.atlasengine.pathfinding.result.PathResult;
-import ca.atlasengine.pathfinding.PathfindingBenchmark;
+import ca.atlasengine.pathfinding.search.EntityPathfinder;
+import ca.atlasengine.pathfinding.search.NavigationRequest;
import ca.atlasengine.pathfinding.search.SearchControl;
import ca.atlasengine.pathfinding.terrain.MobTraversalProfile;
import net.minestom.server.collision.BoundingBox;
@@ -27,7 +27,6 @@
import java.util.ArrayList;
import java.util.List;
-import java.util.Optional;
import java.util.concurrent.TimeUnit;
/** Mass-navigation comparisons using identical validated A* routes. */
@@ -137,7 +136,8 @@ public static class CrowdState {
public void setup() {
buildTerrain();
BoundingBox box = new BoundingBox(width, 2.7, width);
- NavigationProfile profile = NavigationProfile.builder(NavigationMode.GROUND, MobTraversalProfile.DEFAULT, GroundCapabilities.STANDARD).allowBreaching(false).prefersShallowWater(false).avoidSun(false).build();
+ NavigationProfile profile = NavigationProfile.builder(NavigationMode.GROUND,
+ MobTraversalProfile.DEFAULT, GroundCapabilities.STANDARD).build();
Pos target = new Pos(256.5, 1, 0.5);
for (int source = 0; source < sourceCount; source++) {
Pos start = new Pos(source * 16 + 0.5, 1, 0.5);
diff --git a/src/main/java/ca/atlasengine/pathfinding/AsyncEntityPathfindingService.java b/src/main/java/ca/atlasengine/pathfinding/AsyncEntityPathfindingService.java
index e049878..57dd5e2 100644
--- a/src/main/java/ca/atlasengine/pathfinding/AsyncEntityPathfindingService.java
+++ b/src/main/java/ca/atlasengine/pathfinding/AsyncEntityPathfindingService.java
@@ -1,6 +1,13 @@
package ca.atlasengine.pathfinding;
-import ca.atlasengine.pathfinding.metrics.NavigationMetrics;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.search.CancellationSource;import ca.atlasengine.pathfinding.search.EntityPathfinder;import ca.atlasengine.pathfinding.search.NavigationRequest;import ca.atlasengine.pathfinding.search.SearchControl;import java.util.Set;
+import ca.atlasengine.pathfinding.metrics.NavigationMetrics;
+import ca.atlasengine.pathfinding.result.PathResult;
+import ca.atlasengine.pathfinding.search.CancellationSource;
+import ca.atlasengine.pathfinding.search.EntityPathfinder;
+import ca.atlasengine.pathfinding.search.NavigationRequest;
+import ca.atlasengine.pathfinding.search.SearchControl;
+
+import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
@@ -41,31 +48,17 @@ public final class AsyncEntityPathfindingService implements AutoCloseable {
private final ConcurrentHashMap
*/
public class EntityNavigationController implements AutoCloseable {
- private static final long MICROS_PER_TICK = 50_000;
+ /** Submission priority of a replan that leaves the entity with no route. */
+ private static final int ROUTELESS_PRIORITY = 100;
+ private static final int REPLAN_PRIORITY = 10;
private static final WalkMovementExecutor WALK = new WalkMovementExecutor();
private static final ClimbMovementExecutor CLIMB = new ClimbMovementExecutor();
private static final MovementExecutor VOLUME = new VolumeMovementExecutor();
@@ -56,7 +72,8 @@ public class EntityNavigationController implements AutoCloseable {
new PlatformJumpMovementExecutor();
private final Entity entity;
- private final PathSubmitter pathSubmitter;
+ private final NavigationRequestFactory requests;
+ private final ControllerRequestSubmitter submitter;
private final NavigationProfile navigationProfile;
private NavigationProfile activeNavigationProfile;
private final double movementPerTick;
@@ -101,90 +118,30 @@ public class EntityNavigationController implements AutoCloseable {
private int platformJumpAlignmentNode = -1;
private int platformJumpAlignmentTicks;
private int lastGroundImpulseNode = -1;
+ /** Set from any thread by {@link #blockChanged}, consumed by tick(). */
+ private volatile boolean blockChangePending;
/**
- * Creates a controller backed by the modular movement-model dispatcher.
+ * Controllers are created by {@link NavigationSystem#controller(Entity)}
+ * and {@link NavigationSystem#controllerBuilder(Entity)}, which wire them
+ * to the system's admission layer and event node.
*
- *
The supplied profile selects ground, water, flying, amphibious, or
- * wall-climber planning. Ground and wall-climber profiles retain normal
- * collision-aware walking, while water and flying profiles follow the
- * path's three-dimensional waypoints.
+ * @param refreshBuiltinProfile re-derive the built-in profile from the
+ * entity's live metadata for every search
+ * rather than keep {@code profile}
*/
- public EntityNavigationController(Entity entity,
- AsyncEntityPathfindingService service,
- NavigationProfile profile,
- double movementPerTick) {
- this(entity, service, profile, movementPerTick,
- MovementExecutionMode.PHYSICS_VELOCITY);
- }
-
- public EntityNavigationController(
- Entity entity, AsyncEntityPathfindingService service,
- NavigationProfile profile, double movementPerTick,
- MovementExecutionMode movementExecutionMode) {
- this(entity, service, profile, movementPerTick, false,
- movementExecutionMode,
- NavigationRequestFactory.entityRandomOrNull(entity));
- }
-
- /**
- * Creates a modular controller with an authoritative tick-thread random
- * stream for small-flyer start sampling.
- *
- *
Pass the entity AI's own random source when the integration exposes
- * one. The normal constructors retain a stable UUID-derived fallback
- * because Minestom entities do not currently expose such a stream.
- */
- public EntityNavigationController(
- Entity entity, AsyncEntityPathfindingService service,
- NavigationProfile profile, double movementPerTick,
- MovementExecutionMode movementExecutionMode,
- RandomGenerator startNodeRandom) {
- this(entity, service, profile, movementPerTick,
- false, movementExecutionMode, startNodeRandom);
- }
-
- EntityNavigationController(
- Entity entity, AsyncEntityPathfindingService service,
- NavigationProfile profile, double movementPerTick,
- MovementExecutionMode movementExecutionMode,
- ControllerRequestSubmitter controllerSubmitter,
- ControllerSchedulingOptions schedulingOptions) {
- this(entity, service, profile, movementPerTick, false,
- movementExecutionMode,
- NavigationRequestFactory.entityRandomOrNull(entity),
- controllerSubmitter, schedulingOptions);
- }
-
- /**
- * Submits straight to the service, bypassing the bounded admission layer
- * that {@link NavigationSystem}-owned controllers are given.
- */
- private EntityNavigationController(Entity entity,
- AsyncEntityPathfindingService service,
- NavigationProfile profile,
- double movementPerTick,
- boolean refreshBuiltinProfile,
- MovementExecutionMode movementExecutionMode,
- RandomGenerator startNodeRandom) {
- this(entity, service, profile, movementPerTick, refreshBuiltinProfile,
- movementExecutionMode, startNodeRandom,
- (key, request, priority) -> service.submitLatest(key, request),
- ControllerSchedulingOptions.direct(service.parallelism()));
- }
-
- private EntityNavigationController(Entity entity,
- AsyncEntityPathfindingService service,
- NavigationProfile profile,
- double movementPerTick,
- boolean refreshBuiltinProfile,
- MovementExecutionMode movementExecutionMode,
- RandomGenerator startNodeRandom,
- ControllerRequestSubmitter controllerSubmitter,
- ControllerSchedulingOptions schedulingOptions) {
- if (entity == null || service == null || profile == null
+ EntityNavigationController(Entity entity,
+ NavigationMetrics metrics,
+ NavigationProfile profile,
+ double movementPerTick,
+ boolean refreshBuiltinProfile,
+ MovementExecutionMode movementExecutionMode,
+ RandomGenerator startNodeRandom,
+ ControllerRequestSubmitter submitter,
+ ControllerSchedulingOptions schedulingOptions) {
+ if (entity == null || metrics == null || profile == null
|| movementExecutionMode == null
- || startNodeRandom == null || controllerSubmitter == null
+ || startNodeRandom == null || submitter == null
|| schedulingOptions == null
|| !Double.isFinite(movementPerTick) || movementPerTick <= 0) {
throw new IllegalArgumentException("invalid controller arguments");
@@ -192,21 +149,12 @@ private EntityNavigationController(Entity entity,
this.entity = entity;
this.refreshBuiltinProfile = refreshBuiltinProfile;
this.movementExecutionMode = movementExecutionMode;
- this.metrics = service.metrics();
+ this.metrics = metrics;
this.schedulingOptions = schedulingOptions;
this.progress = new ProgressMonitor(movementPerTick);
- NavigationRequestFactory requests = new NavigationRequestFactory(
+ this.requests = new NavigationRequestFactory(
entity, refreshBuiltinProfile, startNodeRandom);
- this.pathSubmitter = (key, instance, start, destination, box,
- influences, modifiers) -> {
- NavigationProfile effective = snapshotDynamicTerrain(
- modifiers.applyTo(currentBaseProfile()));
- NavigationRequest request = requests.build(
- effective, instance, start, destination, box, influences,
- replanner.searchRangeMultiplier(), geometry.isEntityInWater());
- return controllerSubmitter.submit(
- key, request, routeExhausted() ? 100 : 10);
- };
+ this.submitter = submitter;
this.navigationProfile = profile;
this.activeNavigationProfile = profile;
this.movementPerTick = movementPerTick;
@@ -214,49 +162,6 @@ private EntityNavigationController(Entity entity,
profile.mode() == NavigationMode.WALL_CLIMBER;
}
- /**
- * Selects the baseline navigation profile for the entity type.
- */
- public static EntityNavigationController builtin(
- Entity entity, AsyncEntityPathfindingService service,
- double movementPerTick) {
- return builtin(entity, service, movementPerTick,
- NavigationRequestFactory.entityRandomOrNull(entity));
- }
-
- /**
- * Selects the baseline profile while sampling small-flyer starts from the
- * supplied entity AI random stream.
- */
- public static EntityNavigationController builtin(
- Entity entity, AsyncEntityPathfindingService service,
- double movementPerTick, RandomGenerator startNodeRandom) {
- return new EntityNavigationController(entity, service,
- BuiltinNavigationProfiles.forEntity(entity),
- movementPerTick, true,
- MovementExecutionMode.PHYSICS_VELOCITY, startNodeRandom);
- }
-
- static EntityNavigationController builtin(
- Entity entity, AsyncEntityPathfindingService service,
- double movementPerTick, ControllerRequestSubmitter submitter,
- ControllerSchedulingOptions options) {
- return builtin(entity, service, movementPerTick,
- NavigationRequestFactory.entityRandomOrNull(entity),
- submitter, options);
- }
-
- static EntityNavigationController builtin(
- Entity entity, AsyncEntityPathfindingService service,
- double movementPerTick, RandomGenerator random,
- ControllerRequestSubmitter submitter,
- ControllerSchedulingOptions options) {
- return new EntityNavigationController(entity, service,
- BuiltinNavigationProfiles.forEntity(entity), movementPerTick,
- true, MovementExecutionMode.PHYSICS_VELOCITY, random,
- submitter, options);
- }
-
public void moveTo(Point newTarget) {
moveTo(newTarget, List.of(), NavigationModifiers.NONE);
}
@@ -286,27 +191,15 @@ public void moveTo(Point newTarget, List influences,
transition(NavigationState.FAILED);
return;
}
- target = newTarget;
- travelRequired = !goalReached();
- targetUnreachable = false;
- unreachableRecorded = false;
- waypointRecoveryAttempted = false;
- routeStop = PathStop.NOT_SEARCHED;
- activeInfluences = List.copyOf(influences);
- activeModifiers = modifiers;
- activeNavigationProfile = snapshotDynamicTerrain(
- modifiers.applyTo(currentBaseProfile()));
- if (geometry.movesThroughVolume()) {
- suppressGravity();
- }
+ beginNavigation(newTarget, List.copyOf(influences), modifiers,
+ snapshotDynamicTerrain(modifiers.applyTo(currentBaseProfile())));
// Only moveTo() abandons the route the entity is on and stops the
// motion that route was driving; follow() keeps both. A route that
// can still be followed is kept until its replacement lands, so
// retargeting every tick never leaves the entity standing still.
if (routeExhausted()) {
resetRoute();
- nodes = List.of();
- nodeIndex = 0;
+ dropRoute();
stopDrivenMotion();
transition(NavigationState.COMPUTING);
} else {
@@ -325,11 +218,7 @@ public void follow(NavigationPlan plan) {
if (plan == null || !plan.usable()) {
throw new IllegalArgumentException("usable plan required");
}
- var entityBox = entity.getBoundingBox();
- var plannedBox = plan.boundingBox();
- if (Math.abs(entityBox.width() - plannedBox.width()) > 1.0e-6
- || Math.abs(entityBox.height() - plannedBox.height()) > 1.0e-6
- || Math.abs(entityBox.depth() - plannedBox.depth()) > 1.0e-6) {
+ if (!sameDimensions(entity.getBoundingBox(), plan.boundingBox())) {
throw new IllegalArgumentException("plan bounding box does not match entity");
}
observeSearchLanding();
@@ -349,20 +238,9 @@ public void follow(NavigationPlan plan) {
"entity is too far from plan start");
}
}
- if (pending != null) pending.cancel(true);
- pending = null;
- target = plan.target();
- travelRequired = !goalReached();
- targetUnreachable = false;
- unreachableRecorded = false;
- waypointRecoveryAttempted = false;
- // A replayed plan carries a status but no search reason, so the
- // chaining rules fall back to the terminal radius for it.
- routeStop = PathStop.NOT_SEARCHED;
- activeInfluences = List.of();
- activeModifiers = NavigationModifiers.NONE;
- activeNavigationProfile = plan.profile();
- if (geometry.movesThroughVolume()) suppressGravity();
+ cancelPending();
+ beginNavigation(plan.target(), List.of(), NavigationModifiers.NONE,
+ plan.profile());
generation++;
if (!spliced) {
nodes = processed.nodes();
@@ -375,6 +253,45 @@ public void follow(NavigationPlan plan) {
? NavigationState.FOLLOWING : NavigationState.PARTIAL);
}
+ /**
+ * Resets everything a new navigation inherits from the last one. The
+ * search reason starts unknown: a replayed plan carries a status but no
+ * reason, so the chaining rules fall back to the terminal radius for it.
+ */
+ private void beginNavigation(Point newTarget,
+ List influences,
+ NavigationModifiers modifiers,
+ NavigationProfile profile) {
+ target = newTarget;
+ // Judged before the profile changes, against the one being left.
+ travelRequired = !goalReached();
+ targetUnreachable = false;
+ unreachableRecorded = false;
+ waypointRecoveryAttempted = false;
+ routeStop = PathStop.NOT_SEARCHED;
+ activeInfluences = influences;
+ activeModifiers = modifiers;
+ activeNavigationProfile = profile;
+ if (geometry.movesThroughVolume()) suppressGravity();
+ }
+
+ private static boolean sameDimensions(BoundingBox first,
+ BoundingBox second) {
+ return Math.abs(first.width() - second.width()) <= 1.0e-6
+ && Math.abs(first.height() - second.height()) <= 1.0e-6
+ && Math.abs(first.depth() - second.depth()) <= 1.0e-6;
+ }
+
+ private void cancelPending() {
+ if (pending != null) pending.cancel(true);
+ pending = null;
+ }
+
+ private void dropRoute() {
+ nodes = List.of();
+ nodeIndex = 0;
+ }
+
private void resetRoute() {
progress.resetWaypoint();
activePlatformJumpNode = -1;
@@ -392,30 +309,23 @@ public void tick() {
// discard their controllers. Never let a late search result or movement
// command touch Minestom's detached collision state.
if (entity.isRemoved() || entity.getInstance() == null) {
- if (pending != null) pending.cancel(true);
- pending = null;
- if (state == NavigationState.IDLE) {
- state = NavigationState.CANCELLED;
- } else if (state != NavigationState.COMPLETED
- && state != NavigationState.CANCELLED
- && state != NavigationState.STUCK
- && state != NavigationState.FAILED) {
- transition(NavigationState.CANCELLED);
- }
+ cancelPending();
+ cancelNavigation();
return;
}
// A landed search is read before anything decides to replace it, so
// completed work is never discarded on the tick it arrives.
acceptPending();
+ if (blockChangePending) {
+ blockChangePending = false;
+ if (!routeExhausted()) replanForBlockChange();
+ }
if (replanRequested) recomputePath(RouteReplanEvent.Reason.SHED_RETRY);
// Closing runs before the route is followed and outside it, so a
// manipulated block is still restored once the route ends.
manipulator.closeBehind(entity.getInstance(),
activeNavigationProfile.mobProfile(), entity.getPosition());
- if (target == null || state == NavigationState.COMPLETED
- || state == NavigationState.CANCELLED
- || state == NavigationState.STUCK
- || state == NavigationState.FAILED) {
+ if (target == null || state.terminal()) {
restoreGravity();
stopDrivenMotion();
return;
@@ -466,9 +376,12 @@ private void recomputePath(RouteReplanEvent.Reason reason) {
activeModifiers.applyTo(currentBaseProfile()));
generation++;
replanner.beginSegment(entity.getPosition().distance(destination));
- pending = pathSubmitter.submit(entity.getUuid(), instance,
- planningStart(destination), destination,
- entity.getBoundingBox(), activeInfluences, activeModifiers);
+ NavigationRequest request = requests.build(activeNavigationProfile,
+ instance, planningStart(destination), destination,
+ entity.getBoundingBox(), activeInfluences,
+ replanner.searchRangeMultiplier(), geometry.isEntityInWater());
+ pending = submitter.submit(entity.getUuid(), request,
+ routeExhausted() ? ROUTELESS_PRIORITY : REPLAN_PRIORITY);
// Keep following the last accepted route while the replacement is
// computed. If no usable route exists, COMPUTING remains the only
// meaningful state.
@@ -478,21 +391,12 @@ private void recomputePath(RouteReplanEvent.Reason reason) {
}
}
- /**
- * Whether the search behind the current request may be replaced. Work in
- * flight is left alone until the tail this controller's service actually
- * observes says it will never land, so a chased mob keeps receiving
- * routes however slow searches become. The tail is read only once the
- * floor has already elapsed, so the ordinary path costs nothing.
- */
+ /** Whether the search behind the current request may be replaced. */
private boolean searchSettledOrAbandoned() {
if (tick <= lastSubmitTick || tick < shedBackoffUntilTick) return false;
- if (pending == null) return true;
- int waited = tick - lastSubmitTick;
- int minimumStallTicks = schedulingOptions.minimumSearchStallTicks();
- if (waited < minimumStallTicks) return false;
- long tail = metrics.snapshot().latency().p99Micros() / MICROS_PER_TICK;
- return waited >= Math.max(minimumStallTicks, tail * 4);
+ return pending == null || SearchLatency.abandoned(
+ tick - lastSubmitTick,
+ schedulingOptions.minimumSearchStallTicks(), metrics);
}
/**
@@ -534,8 +438,8 @@ latencyTicks, movementPerTick, startTolerance())) {
* search is submitted, so following costs nothing.
*/
private int observedLatencyTicks() {
- return splicer.expectedLatencyTicks((int) (metrics.snapshot()
- .latency().p90Micros() / MICROS_PER_TICK));
+ return splicer.expectedLatencyTicks((int) SearchLatency.ticks(
+ metrics.snapshot().latency().p90Micros()));
}
/** Records how many ticks the route just applied spent in flight. */
@@ -606,7 +510,7 @@ public void suppressNextSplice() {
}
/** Splice bookkeeping, for tests and operators. */
- public RouteSplicer.Counts spliceCounts() {
+ public SpliceCounts spliceCounts() {
return splicer.counts();
}
@@ -616,28 +520,53 @@ private boolean routeExhausted() {
/**
* Requests a recomputation when a changed block is close enough to affect
- * the unconsumed portion of the current path.
+ * the unconsumed portion of the current path. Call it from the thread
+ * that ticks this controller.
*/
public void onBlockChanged(Point changedBlock) {
- if (changedBlock == null || nodes.isEmpty()
- || nodeIndex >= nodes.size()) return;
- PathNode end = nodes.getLast();
+ if (changedBlock != null && routeMayCross(changedBlock)) {
+ replanForBlockChange();
+ }
+ }
+
+ /**
+ * The thread-safe form {@link NavigationSystem} uses: the edit may come
+ * from any thread and any instance, so it is only judged here and acted
+ * on by the next {@link #tick()}, which is also where its event fires.
+ */
+ void blockChanged(Instance instance, Point changedBlock) {
+ if (entity.getInstance() == instance && routeMayCross(changedBlock)) {
+ blockChangePending = true;
+ }
+ }
+
+ /**
+ * Whether a block lies near enough to the stretch still to be walked to
+ * matter. Reads only the immutable route and the entity position, so a
+ * stale answer from another thread errs toward a harmless extra replan.
+ */
+ private boolean routeMayCross(Point changedBlock) {
+ List route = nodes;
+ int remaining = route.size() - nodeIndex;
+ if (remaining <= 0) return false;
+ PathNode end = route.getLast();
Point position = entity.getPosition();
Point middle = new Vec(
(end.x() + position.x()) * 0.5,
(end.y() + position.y()) * 0.5,
(end.z() + position.z()) * 0.5);
- double remainingNodes = nodes.size() - nodeIndex;
Point changedCenter = new Vec(
changedBlock.blockX() + 0.5,
changedBlock.blockY() + 0.5,
changedBlock.blockZ() + 0.5);
- if (changedCenter.distance(middle) < remainingNodes) {
- // The changed block may lie on the very stretch a splice would
- // retain, so this replan starts from the entity itself.
- spliceSuppressed = true;
- recomputePath(RouteReplanEvent.Reason.BLOCK_CHANGE);
- }
+ return changedCenter.distance(middle) < remaining;
+ }
+
+ private void replanForBlockChange() {
+ // The changed block may lie on the very stretch a splice would
+ // retain, so this replan starts from the entity itself.
+ spliceSuppressed = true;
+ recomputePath(RouteReplanEvent.Reason.BLOCK_CHANGE);
}
private void acceptPending() {
@@ -728,8 +657,7 @@ private PathPostProcessor.Result postProcess(List path) {
*/
private void widenSearchRange() {
replanner.widen();
- nodes = List.of();
- nodeIndex = 0;
+ dropRoute();
recomputePath(RouteReplanEvent.Reason.PARTIAL_CONTINUATION);
}
@@ -743,19 +671,17 @@ private void prefetchPartialContinuation() {
if (state != NavigationState.PARTIAL
|| wallClimberFallback || partialContinuationPrefetched
|| pending != null
- || replanRequested || target == null || nodes.isEmpty()
- || nodeIndex >= nodes.size() || pathTruncatedForSun) {
+ || replanRequested || target == null || routeExhausted()
+ || pathTruncatedForSun) {
return;
}
double safetyTail = Math.max(8,
(observedLatencyTicks() + 2) * movementPerTick);
if (remainingRouteDistance() > safetyTail) return;
- PathNode endpoint = nodes.getLast();
- Point projected = endpoint.asVec();
partialContinuationPrefetched = true;
switch (replanner.continueLongPartialRoute(
- projected, target, nodes, routeStop)) {
+ nodes.getLast().asVec(), target, nodes, routeStop)) {
case WIDEN -> {
replanner.widen();
recomputePath(RouteReplanEvent.Reason.PARTIAL_CONTINUATION);
@@ -770,14 +696,7 @@ private void prefetchPartialContinuation() {
}
private double remainingRouteDistance() {
- double remaining = 0;
- Point cursor = entity.getPosition();
- for (int index = nodeIndex; index < nodes.size(); index++) {
- Point next = nodes.get(index).asVec();
- remaining += cursor.distance(next);
- cursor = next;
- }
- return remaining;
+ return RouteLength.from(entity.getPosition(), nodes, nodeIndex);
}
private NavigationState fallbackState(NavigationState terminal) {
@@ -791,10 +710,7 @@ private NavigationState fallbackState(NavigationState terminal) {
*/
private void transition(NavigationState next) {
if (state == next) return;
- boolean active = switch (state) {
- case IDLE, COMPUTING, FOLLOWING, PARTIAL -> true;
- case COMPLETED, STUCK, FAILED, CANCELLED -> false;
- };
+ boolean active = !state.terminal();
state = next;
if (active) metrics.controllerOutcome(next, travelRequired);
// After the state is published, so a listener reading state() sees the
@@ -840,8 +756,7 @@ private void followPath() {
waypointRecoveryAttempted = true;
splicer.disarm();
spliceSuppressed = true;
- nodes = List.of();
- nodeIndex = 0;
+ dropRoute();
resetRoute();
stopDrivenMotion();
transition(NavigationState.COMPUTING);
@@ -963,16 +878,16 @@ private void restoreGravity() {
}
private boolean goalReached() {
- double tolerance = geometry.alwaysMovesThroughVolume() ? 0.7 : 0.5;
- Point effectiveTarget = target;
- if (target != null && geometry.alwaysMovesThroughVolume()) {
- effectiveTarget = new Vec(
- Math.floor(target.x()) + 0.5,
- Math.floor(target.y()),
- Math.floor(target.z()) + 0.5);
- }
- return effectiveTarget != null && entity.getPosition().distance(effectiveTarget)
- <= Math.max(tolerance, entity.getBoundingBox().width() / 2);
+ if (target == null) return false;
+ // A volume navigator is steered to its target cell's bottom centre.
+ boolean volume = geometry.alwaysMovesThroughVolume();
+ Point goal = volume
+ ? new Vec(Math.floor(target.x()) + 0.5, Math.floor(target.y()),
+ Math.floor(target.z()) + 0.5)
+ : target;
+ double tolerance = Math.max(volume ? 0.7 : 0.5,
+ entity.getBoundingBox().width() / 2);
+ return entity.getPosition().distance(goal) <= tolerance;
}
private NavigationProfile currentBaseProfile() {
@@ -995,7 +910,7 @@ void eventSink(NavigationEventSink sink) {
}
private void emit(Class extends NavigationEvent> type,
- java.util.function.Supplier event) {
+ Supplier event) {
if (events.listening(type)) events.emit(event.get());
}
@@ -1015,8 +930,12 @@ public NavigationState state() {
* reach the goal from where the entity stands and the caller should pick
* another one, while {@link NavigationState#STUCK} means the entity is
* physically wedged on a route it was given. It is only proof of
- * unreachability when the search emptied its frontier; otherwise it is the
- * planner declining to spend another bounded segment on this target.
+ * unreachability when {@link #routeStop()} reads
+ * {@link PathStop#FRONTIER_EXHAUSTED}; otherwise it is the planner
+ * declining to spend another bounded segment on this target. In
+ * particular {@link PathStop#TERRAIN_UNLOADED} means the route ran into
+ * chunks that are not loaded: call {@link #moveTo(Point)} again once they
+ * are.
*
*
Cleared by {@link #moveTo(Point)}, by {@link #follow(NavigationPlan)},
* and by any route that lands afterwards.
@@ -1051,13 +970,29 @@ public List nodes() {
return nodes;
}
+ /**
+ * Abandons any search in flight and restores gravity. A navigation still
+ * in progress ends {@link NavigationState#CANCELLED}; one that already
+ * completed, stalled, or failed keeps that outcome.
+ */
@Override
public void close() {
- if (pending != null) pending.cancel(true);
+ cancelPending();
restoreGravity();
- // A controller closed before it ever navigated has no outcome.
- if (state == NavigationState.IDLE) state = NavigationState.CANCELLED;
- else transition(NavigationState.CANCELLED);
+ cancelNavigation();
+ }
+
+ /**
+ * Ends the navigation in progress as cancelled. A controller that never
+ * navigated has no outcome to report, and one whose navigation already
+ * finished keeps the outcome it reported.
+ */
+ private void cancelNavigation() {
+ if (state == NavigationState.IDLE) {
+ state = NavigationState.CANCELLED;
+ } else if (!state.terminal()) {
+ transition(NavigationState.CANCELLED);
+ }
}
private final class ControllerMovementContext implements MovementContext {
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/EntityTargets.java b/src/main/java/ca/atlasengine/pathfinding/EntityTargets.java
similarity index 89%
rename from src/main/java/ca/atlasengine/pathfinding/internal/EntityTargets.java
rename to src/main/java/ca/atlasengine/pathfinding/EntityTargets.java
index d9abfc4..8af7f75 100644
--- a/src/main/java/ca/atlasengine/pathfinding/internal/EntityTargets.java
+++ b/src/main/java/ca/atlasengine/pathfinding/EntityTargets.java
@@ -1,4 +1,4 @@
-package ca.atlasengine.pathfinding.internal;
+package ca.atlasengine.pathfinding;
import net.minestom.server.collision.BoundingBox;
import net.minestom.server.coordinate.Point;
@@ -7,7 +7,13 @@
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.block.Block;
-/** Target projections used when navigating toward another entity. */
+/**
+ * Where to send a mob that is chasing another entity.
+ *
+ *
An entity's raw position is a poor destination: a target sneaking over an
+ * edge stands above empty air. Pass {@link #supportedPosition(Entity)} to
+ * {@link EntityNavigationController#moveTo} instead.
+ */
public final class EntityTargets {
private static final double CONTACT_EPSILON = 1.0e-4;
diff --git a/src/main/java/ca/atlasengine/pathfinding/NavigationPlan.java b/src/main/java/ca/atlasengine/pathfinding/NavigationPlan.java
index c780d4d..82a059a 100644
--- a/src/main/java/ca/atlasengine/pathfinding/NavigationPlan.java
+++ b/src/main/java/ca/atlasengine/pathfinding/NavigationPlan.java
@@ -1,12 +1,13 @@
package ca.atlasengine.pathfinding;
-import ca.atlasengine.pathfinding.search.EntityPathfinder;import ca.atlasengine.pathfinding.profile.NavigationProfile;
-import ca.atlasengine.pathfinding.search.NavigationRequest;
+import ca.atlasengine.pathfinding.profile.NavigationProfile;
import ca.atlasengine.pathfinding.result.PathNode;
import ca.atlasengine.pathfinding.result.PathNodeCost;
import ca.atlasengine.pathfinding.result.PathResult;
import ca.atlasengine.pathfinding.result.PathStatus;
import ca.atlasengine.pathfinding.result.PathStop;
+import ca.atlasengine.pathfinding.search.EntityPathfinder;
+import ca.atlasengine.pathfinding.search.NavigationRequest;
import net.minestom.server.collision.BoundingBox;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
@@ -51,15 +52,6 @@ public record NavigationPlan(
nodeCosts = List.copyOf(nodeCosts);
}
- /** An unpriced plan, for callers that never ran a node-graph search. */
- public NavigationPlan(
- Point start, Point target, BoundingBox boundingBox,
- NavigationProfile profile, PathStatus status,
- List nodes, int visitedNodes, int examinedNeighbors) {
- this(start, target, boundingBox, profile, status, nodes,
- visitedNodes, examinedNeighbors, List.of());
- }
-
/** Keeps the route, its work counters, and its per-cell charged costs. */
public NavigationPlan(Point start, Point target, BoundingBox boundingBox,
NavigationProfile profile, PathStatus status,
diff --git a/src/main/java/ca/atlasengine/pathfinding/NavigationState.java b/src/main/java/ca/atlasengine/pathfinding/NavigationState.java
index f208617..2c85ff5 100644
--- a/src/main/java/ca/atlasengine/pathfinding/NavigationState.java
+++ b/src/main/java/ca/atlasengine/pathfinding/NavigationState.java
@@ -8,5 +8,16 @@ public enum NavigationState {
PARTIAL,
STUCK,
FAILED,
- CANCELLED
+ CANCELLED;
+
+ /**
+ * Whether this state ends a navigation. A controller in a terminal state
+ * stays there until it is given a new target or plan.
+ */
+ public boolean terminal() {
+ return switch (this) {
+ case COMPLETED, STUCK, FAILED, CANCELLED -> true;
+ case IDLE, COMPUTING, FOLLOWING, PARTIAL -> false;
+ };
+ }
}
diff --git a/src/main/java/ca/atlasengine/pathfinding/NavigationStrategy.java b/src/main/java/ca/atlasengine/pathfinding/NavigationStrategy.java
index 4d4b6aa..3962ec5 100644
--- a/src/main/java/ca/atlasengine/pathfinding/NavigationStrategy.java
+++ b/src/main/java/ca/atlasengine/pathfinding/NavigationStrategy.java
@@ -23,9 +23,8 @@ public enum NavigationStrategy {
*
This is a preference over the dispatch decision, never over the
* parity gate: it cannot make a shared plan appear. A shared plan is
* served only when the region is already promoted and holds a certified
- * ordinary-A* plan for this exact source and destination class that still
- * passes {@link
- * ca.atlasengine.pathfinding.internal.adaptive.NavigationPlanParity}. A missing
+ * ordinary-A* plan for this exact source and destination class that the
+ * shared composition still reproduces waypoint for waypoint. A missing
* certificate, a failed parity check, a moved source, an exhausted region,
* or a disabled mesh silently runs the ordinary search instead. There is
* no mode that serves an uncertified shared plan.
diff --git a/src/main/java/ca/atlasengine/pathfinding/NavigationSystem.java b/src/main/java/ca/atlasengine/pathfinding/NavigationSystem.java
index 29395a1..383ee33 100644
--- a/src/main/java/ca/atlasengine/pathfinding/NavigationSystem.java
+++ b/src/main/java/ca/atlasengine/pathfinding/NavigationSystem.java
@@ -1,22 +1,28 @@
package ca.atlasengine.pathfinding;
-import ca.atlasengine.pathfinding.metrics.NavigationMetrics;import ca.atlasengine.pathfinding.metrics.NavigationMetricsSnapshot;import ca.atlasengine.pathfinding.profile.NavigationProfile;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.search.NavigationRequest;import ca.atlasengine.pathfinding.adaptive.SharedMeshNavigation;
+import ca.atlasengine.pathfinding.adaptive.SharedMeshNavigation;
import ca.atlasengine.pathfinding.adaptive.SharedMeshOptions;
-import ca.atlasengine.pathfinding.influence.NavigationInfluence;
+import ca.atlasengine.pathfinding.event.NavigationEvent;
+import ca.atlasengine.pathfinding.event.NavigationEventSink;
+import ca.atlasengine.pathfinding.internal.movement.NavigationRequestFactory;
+import ca.atlasengine.pathfinding.metrics.NavigationMetrics;
+import ca.atlasengine.pathfinding.metrics.NavigationMetricsSnapshot;
+import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;
+import ca.atlasengine.pathfinding.profile.NavigationProfile;
+import ca.atlasengine.pathfinding.result.PathResult;
+import ca.atlasengine.pathfinding.search.NavigationRequest;
import net.minestom.server.coordinate.Point;
import net.minestom.server.entity.Entity;
-
+import net.minestom.server.event.EventFilter;
import net.minestom.server.event.EventListener;
+import net.minestom.server.event.EventNode;
import net.minestom.server.event.instance.InstanceBlockUpdateEvent;
import net.minestom.server.instance.Instance;
-import ca.atlasengine.pathfinding.event.NavigationEvent;
-import ca.atlasengine.pathfinding.event.NavigationEventSink;
-import net.minestom.server.event.EventFilter;
-import net.minestom.server.event.EventNode;
+
import java.util.Collections;
-import java.util.WeakHashMap;
import java.util.List;
import java.util.Set;
+import java.util.WeakHashMap;
import java.util.concurrent.CompletableFuture;
import java.util.random.RandomGenerator;
@@ -91,52 +97,81 @@ public static Builder builder() {
/** Creates a baseline controller using this system's default speed. */
public EntityNavigationController controller(Entity entity) {
- return controller(entity, movementPerTick);
- }
-
- /** Creates a baseline controller with an explicit movement speed. */
- public EntityNavigationController controller(
- Entity entity, double movementPerTick) {
- validate(entity, movementPerTick);
- return track(EntityNavigationController.builtin(
- entity, service, movementPerTick,
- controllerScheduler::submitLatest, controllerOptions));
+ return controllerBuilder(entity).build();
}
/**
- * Creates a controller with an explicit traversal profile. This is the integration point
- * for model-driven or proxy entities whose Minestom carrier type does not describe how the
- * visible mob should navigate (for example an invisible parrot carrying a ground model).
+ * Starts a controller for {@code entity} with a fixed profile, another
+ * speed, a movement mode, or the entity AI's random stream. Every
+ * controller a system creates submits through its bounded admission layer
+ * and reports to its event node.
*/
- public EntityNavigationController controller(
- Entity entity, NavigationProfile profile, double movementPerTick) {
- validate(entity, movementPerTick);
- if (profile == null) {
- throw new IllegalArgumentException("invalid controller arguments");
- }
- return track(new EntityNavigationController(
- entity, service, profile, movementPerTick,
- MovementExecutionMode.PHYSICS_VELOCITY,
- controllerScheduler::submitLatest, controllerOptions));
+ public ControllerBuilder controllerBuilder(Entity entity) {
+ return new ControllerBuilder(entity);
}
/**
- * Creates a baseline controller using the mob AI's authoritative random
- * stream for randomized start sampling.
+ * Settings for one controller. Unset settings take the built-in profile of
+ * the entity's type, re-derived from its live metadata for every search;
+ * this system's default speed; velocity-driven movement; and a
+ * start-sampling random seeded from the entity's UUID.
*/
- public EntityNavigationController controller(
- Entity entity, RandomGenerator random) {
- validate(entity, movementPerTick);
- if (random == null) throw new IllegalArgumentException("random");
- return track(EntityNavigationController.builtin(
- entity, service, movementPerTick, random,
- controllerScheduler::submitLatest, controllerOptions));
- }
+ public final class ControllerBuilder {
+ private final Entity entity;
+ private NavigationProfile profile;
+ private double movementPerTick = NavigationSystem.this.movementPerTick;
+ private MovementExecutionMode executionMode =
+ MovementExecutionMode.PHYSICS_VELOCITY;
+ private RandomGenerator startNodeRandom;
+
+ private ControllerBuilder(Entity entity) {
+ if (entity == null) throw new IllegalArgumentException("entity");
+ this.entity = entity;
+ }
+
+ /**
+ * A fixed traversal profile in place of the entity type's built-in
+ * one, for entities whose Minestom type does not describe how they
+ * should navigate.
+ */
+ public ControllerBuilder profile(NavigationProfile value) {
+ if (value == null) throw new IllegalArgumentException("profile");
+ profile = value;
+ return this;
+ }
+
+ public ControllerBuilder movementPerTick(double value) {
+ if (!Double.isFinite(value) || value <= 0) {
+ throw new IllegalArgumentException("movementPerTick");
+ }
+ movementPerTick = value;
+ return this;
+ }
+
+ public ControllerBuilder executionMode(MovementExecutionMode value) {
+ if (value == null) throw new IllegalArgumentException("executionMode");
+ executionMode = value;
+ return this;
+ }
+
+ /**
+ * The entity AI's own random stream, used to sample small flyers'
+ * start nodes on the tick thread.
+ */
+ public ControllerBuilder startNodeRandom(RandomGenerator value) {
+ if (value == null) throw new IllegalArgumentException("startNodeRandom");
+ startNodeRandom = value;
+ return this;
+ }
- private static void validate(Entity entity, double movementPerTick) {
- if (entity == null || !Double.isFinite(movementPerTick)
- || movementPerTick <= 0) {
- throw new IllegalArgumentException("invalid controller arguments");
+ public EntityNavigationController build() {
+ boolean builtin = profile == null;
+ return track(new EntityNavigationController(entity, metrics,
+ builtin ? BuiltinNavigationProfiles.forEntity(entity) : profile,
+ movementPerTick, builtin, executionMode,
+ startNodeRandom != null ? startNodeRandom
+ : NavigationRequestFactory.entityRandomOrNull(entity),
+ controllerScheduler::submitLatest, controllerOptions));
}
}
@@ -229,20 +264,23 @@ public synchronized EventNode eventNode() {
}
/**
- * Replans any route this system owns that the changed block may cross.
+ * Replans any route this system owns in {@code instance} that the
+ * changed block may cross.
*
*
A controller decides for itself whether the block is near enough to
* the stretch it has left to walk, so calling this for every block change
- * is cheap and safe.
+ * is cheap and safe. It may be called from any thread: affected
+ * controllers replan on their next {@link
+ * EntityNavigationController#tick()}, on the thread that ticks them.
*/
- public void notifyBlockChanged(Point changedBlock) {
- if (changedBlock == null) return;
+ public void notifyBlockChanged(Instance instance, Point changedBlock) {
+ if (instance == null || changedBlock == null) return;
List snapshot;
synchronized (controllers) {
snapshot = List.copyOf(controllers);
}
for (EntityNavigationController controller : snapshot) {
- controller.onBlockChanged(changedBlock);
+ controller.blockChanged(instance, changedBlock);
}
}
@@ -250,16 +288,17 @@ public void notifyBlockChanged(Point changedBlock) {
* Subscribes to {@code instance} so block changes replan affected routes
* without the caller wiring anything.
*
- *
Listeners run on whichever thread applied the block change, which for
- * ordinary instance edits is the tick thread that also ticks the
- * controllers. Returns the registered listener so it can be removed.
+ *
Only controllers whose entity is in {@code instance} react, and they
+ * replan on their own next tick whichever thread applied the change.
+ * Returns the registered listener so it can be removed.
*/
public EventListener watchBlockChanges(
Instance instance) {
if (instance == null) throw new IllegalArgumentException("instance");
EventListener listener =
EventListener.of(InstanceBlockUpdateEvent.class,
- event -> notifyBlockChanged(event.getBlockPosition()));
+ event -> notifyBlockChanged(
+ instance, event.getBlockPosition()));
instance.eventNode().addListener(listener);
return listener;
}
@@ -317,8 +356,10 @@ public static final class Builder {
private int queueCapacity = 256;
private int maximumDeferredControllerRequests = 16_384;
private int maximumConcurrentControllerSearches = Integer.MAX_VALUE;
- private int minimumSearchStallTicks = 60;
- private int shedBackoffTicks = 20;
+ private int minimumSearchStallTicks =
+ ControllerSchedulingOptions.DEFAULT_MINIMUM_SEARCH_STALL_TICKS;
+ private int shedBackoffTicks =
+ ControllerSchedulingOptions.DEFAULT_SHED_BACKOFF_TICKS;
private double movementPerTick = 0.2;
private SharedMeshOptions sharedMesh = SharedMeshOptions.DISABLED;
private NavigationMetrics metrics;
diff --git a/src/main/java/ca/atlasengine/pathfinding/PathDebugRenderer.java b/src/main/java/ca/atlasengine/pathfinding/PathDebugRenderer.java
index cca25f6..31436f2 100644
--- a/src/main/java/ca/atlasengine/pathfinding/PathDebugRenderer.java
+++ b/src/main/java/ca/atlasengine/pathfinding/PathDebugRenderer.java
@@ -1,6 +1,7 @@
package ca.atlasengine.pathfinding;
-import ca.atlasengine.pathfinding.result.PathNode;import net.minestom.server.coordinate.Point;
+import ca.atlasengine.pathfinding.result.PathNode;
+import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.instance.Instance;
import net.minestom.server.network.packet.server.play.ParticlePacket;
@@ -69,7 +70,7 @@ public int render(Instance instance, NavigationPlan plan) {
if (instance == null || plan == null) {
throw new IllegalArgumentException("instance and plan");
}
- return render(instance, plan.result().nodes(), 0);
+ return render(instance, plan.nodes(), 0);
}
private int render(Instance instance, List nodes, int nodeIndex) {
diff --git a/src/main/java/ca/atlasengine/pathfinding/adaptive/SharedMeshHandle.java b/src/main/java/ca/atlasengine/pathfinding/adaptive/SharedMeshHandle.java
index 0711ea2..a5c73f0 100644
--- a/src/main/java/ca/atlasengine/pathfinding/adaptive/SharedMeshHandle.java
+++ b/src/main/java/ca/atlasengine/pathfinding/adaptive/SharedMeshHandle.java
@@ -1,6 +1,7 @@
package ca.atlasengine.pathfinding.adaptive;
import ca.atlasengine.pathfinding.NavigationPlan;
+
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
diff --git a/src/main/java/ca/atlasengine/pathfinding/adaptive/SharedMeshNavigation.java b/src/main/java/ca/atlasengine/pathfinding/adaptive/SharedMeshNavigation.java
index ed1dc36..c963a0a 100644
--- a/src/main/java/ca/atlasengine/pathfinding/adaptive/SharedMeshNavigation.java
+++ b/src/main/java/ca/atlasengine/pathfinding/adaptive/SharedMeshNavigation.java
@@ -1,11 +1,11 @@
package ca.atlasengine.pathfinding.adaptive;
import ca.atlasengine.pathfinding.NavigationPlan;
-import ca.atlasengine.pathfinding.internal.adaptive.SharedMeshCoordinator;
-import ca.atlasengine.pathfinding.profile.NavigationModifiers;
import ca.atlasengine.pathfinding.NavigationStrategy;
import ca.atlasengine.pathfinding.NavigationSystem;
import ca.atlasengine.pathfinding.influence.NavigationInfluence;
+import ca.atlasengine.pathfinding.internal.adaptive.SharedMeshCoordinator;
+import ca.atlasengine.pathfinding.profile.NavigationModifiers;
import net.minestom.server.entity.Entity;
import net.minestom.server.instance.block.Block;
diff --git a/src/main/java/ca/atlasengine/pathfinding/adaptive/SharedMeshOptions.java b/src/main/java/ca/atlasengine/pathfinding/adaptive/SharedMeshOptions.java
index 811a879..0af201f 100644
--- a/src/main/java/ca/atlasengine/pathfinding/adaptive/SharedMeshOptions.java
+++ b/src/main/java/ca/atlasengine/pathfinding/adaptive/SharedMeshOptions.java
@@ -1,6 +1,7 @@
package ca.atlasengine.pathfinding.adaptive;
-import ca.atlasengine.pathfinding.NavigationSystem;import ca.atlasengine.pathfinding.search.EntityTraversalState;
+import ca.atlasengine.pathfinding.NavigationSystem;
+import ca.atlasengine.pathfinding.search.EntityTraversalState;
/**
* The explicit opt-in and policy for shared-mesh planning.
*
diff --git a/src/main/java/ca/atlasengine/pathfinding/adaptive/SharedMeshPursuit.java b/src/main/java/ca/atlasengine/pathfinding/adaptive/SharedMeshPursuit.java
index 467ac56..a5351b9 100644
--- a/src/main/java/ca/atlasengine/pathfinding/adaptive/SharedMeshPursuit.java
+++ b/src/main/java/ca/atlasengine/pathfinding/adaptive/SharedMeshPursuit.java
@@ -1,31 +1,30 @@
package ca.atlasengine.pathfinding.adaptive;
-import net.minestom.server.instance.block.Block;
+import ca.atlasengine.pathfinding.EntityNavigationController;
+import ca.atlasengine.pathfinding.EntityTargets;
import ca.atlasengine.pathfinding.NavigationPlan;
-import ca.atlasengine.pathfinding.internal.EntityTargets;
+import ca.atlasengine.pathfinding.NavigationState;
+import ca.atlasengine.pathfinding.NavigationSystem;
+import ca.atlasengine.pathfinding.influence.NavigationInfluence;
+import ca.atlasengine.pathfinding.internal.SearchLatency;
import ca.atlasengine.pathfinding.internal.adaptive.TargetCell;
+import ca.atlasengine.pathfinding.internal.movement.NavigationRequestFactory;
+import ca.atlasengine.pathfinding.metrics.SpliceCounts;
import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;
-import ca.atlasengine.pathfinding.EntityNavigationController;
-import ca.atlasengine.pathfinding.search.EntityTraversalState;
import ca.atlasengine.pathfinding.profile.NavigationModifiers;
import ca.atlasengine.pathfinding.profile.NavigationProfile;
-import ca.atlasengine.pathfinding.search.NavigationRequest;
-import ca.atlasengine.pathfinding.NavigationState;
-import ca.atlasengine.pathfinding.NavigationSystem;
import ca.atlasengine.pathfinding.result.PathNode;
import ca.atlasengine.pathfinding.result.PathStatus;
-import ca.atlasengine.pathfinding.influence.NavigationInfluence;
-import ca.atlasengine.pathfinding.internal.movement.RouteSplicer;
+import ca.atlasengine.pathfinding.search.EntityTraversalState;
+import ca.atlasengine.pathfinding.search.NavigationRequest;
import ca.atlasengine.pathfinding.terrain.BlockTraversalData;
import net.minestom.server.coordinate.Point;
-import net.minestom.server.entity.attribute.Attribute;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.LivingEntity;
import net.minestom.server.instance.Instance;
import java.util.List;
import java.util.Objects;
-import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletionException;
@@ -52,8 +51,6 @@
* search may already have read, abandons work in flight.
*/
public final class SharedMeshPursuit implements AutoCloseable {
- private static final long MICROS_PER_TICK = 50_000;
-
private final NavigationSystem system;
private final Entity actor;
private final Entity target;
@@ -173,7 +170,7 @@ public int generation() {
}
/** Splice bookkeeping of the follower, for tests and operators. */
- public RouteSplicer.Counts spliceCounts() {
+ public SpliceCounts spliceCounts() {
return controller.spliceCounts();
}
@@ -200,12 +197,9 @@ private void submit() {
}
if (planning != null) planning.close();
Instance instance = actor.getInstance();
- NavigationProfile base = BuiltinNavigationProfiles.forEntity(actor);
- NavigationProfile profile = modifiers.applyTo(base);
- double range = actor instanceof LivingEntity living
- ? living.getAttribute(Attribute.FOLLOW_RANGE).getValue() : 16;
- range = Math.max(range,
- BuiltinNavigationProfiles.requiredPathLength(actor));
+ NavigationProfile profile = modifiers.applyTo(
+ BuiltinNavigationProfiles.forEntity(actor));
+ double range = NavigationRequestFactory.searchRange(actor, true);
// Plan from where the actor will stand when this search lands, not
// from where it stands now. The follower arms the matching splice and
// hands back the actor's own position whenever it cannot carry one.
@@ -213,10 +207,9 @@ private void submit() {
Point start = controller.planningStart(destination);
boolean inWater = BlockTraversalData.hasWaterFluid(
instance.getBlock(start));
- Set standable = profile.mobProfile().standsOnLava()
- ? Set.of(Block.LAVA) : Set.of();
EntityTraversalState state = new EntityTraversalState(
- actor.isOnGround(), inWater, standable,
+ actor.isOnGround(), inWater,
+ profile.mobProfile().standableFluids(),
instance.getCachedDimensionType().minY(),
instance.getCachedDimensionType().maxY() - 1, List.of());
NavigationRequest request = NavigationRequest.builder(
@@ -225,26 +218,25 @@ private void submit() {
.maxPathLength(range).nodeSearchRange(range)
.influences(influences).entityState(state).build();
requestedTargetCell = TargetCell.of(destination);
- planning = system.sharedMesh().plan(SharedMeshRequest.builder(request).actor(actor.getUuid()).target(target.getUuid()).worldRevision(worldRevision).currentTick(currentTick).build());
+ planning = system.sharedMesh().plan(SharedMeshRequest.builder(request)
+ .actor(actor.getUuid())
+ .target(target.getUuid())
+ .worldRevision(worldRevision)
+ .currentTick(currentTick)
+ .build());
submittedTick = currentTick;
consumed = false;
replanRequested = false;
}
/**
- * Whether the search behind the current request may be replaced. Work in
- * flight is left alone until the tail the system observes says it
- * will never land, so replanning is paced by search completion and a
- * chased mob keeps receiving routes however slow searches become.
+ * Whether the search behind the current request may be replaced, so
+ * replanning is paced by search completion rather than target movement.
*/
private boolean searchSettledOrAbandoned() {
- if (consumed) return true;
- long waited = currentTick - submittedTick;
- long minimumStallTicks = system.options().minimumSearchStallTicks();
- if (waited < minimumStallTicks) return false;
- long tail = system.metricsSnapshot().latency().p99Micros()
- / MICROS_PER_TICK;
- return waited >= Math.max(minimumStallTicks, tail * 4);
+ return consumed || SearchLatency.abandoned(
+ currentTick - submittedTick,
+ system.options().minimumSearchStallTicks(), system.metrics());
}
private void applyCompletedPlan() {
diff --git a/src/main/java/ca/atlasengine/pathfinding/adaptive/SharedMeshRequest.java b/src/main/java/ca/atlasengine/pathfinding/adaptive/SharedMeshRequest.java
index 9baea50..126b580 100644
--- a/src/main/java/ca/atlasengine/pathfinding/adaptive/SharedMeshRequest.java
+++ b/src/main/java/ca/atlasengine/pathfinding/adaptive/SharedMeshRequest.java
@@ -1,7 +1,7 @@
package ca.atlasengine.pathfinding.adaptive;
-import ca.atlasengine.pathfinding.search.NavigationRequest;
import ca.atlasengine.pathfinding.NavigationStrategy;
+import ca.atlasengine.pathfinding.search.NavigationRequest;
/**
* One shared-mesh planning request: the search itself plus the routing
diff --git a/src/main/java/ca/atlasengine/pathfinding/adaptive/SharedRouteMesh.java b/src/main/java/ca/atlasengine/pathfinding/adaptive/SharedRouteMesh.java
index 349dd05..311165f 100644
--- a/src/main/java/ca/atlasengine/pathfinding/adaptive/SharedRouteMesh.java
+++ b/src/main/java/ca/atlasengine/pathfinding/adaptive/SharedRouteMesh.java
@@ -2,6 +2,7 @@
import ca.atlasengine.pathfinding.NavigationPlan;
import ca.atlasengine.pathfinding.profile.NavigationProfile;
+import net.minestom.server.collision.BoundingBox;
import java.util.ArrayList;
import java.util.HashMap;
@@ -10,7 +11,6 @@
import java.util.Optional;
import java.util.PriorityQueue;
import java.util.concurrent.ConcurrentHashMap;
-import net.minestom.server.collision.BoundingBox;
/**
* Immutable directed mesh of collision-validated route segments.
diff --git a/src/main/java/ca/atlasengine/pathfinding/event/NavigationCancelledEvent.java b/src/main/java/ca/atlasengine/pathfinding/event/NavigationCancelledEvent.java
index ee75913..bd73e7d 100644
--- a/src/main/java/ca/atlasengine/pathfinding/event/NavigationCancelledEvent.java
+++ b/src/main/java/ca/atlasengine/pathfinding/event/NavigationCancelledEvent.java
@@ -2,7 +2,10 @@
import ca.atlasengine.pathfinding.EntityNavigationController;
-/** A navigation was cancelled, including by close(). */
+/**
+ * A navigation in progress was cancelled, including by closing its controller
+ * or removing its entity. A navigation that had already finished is not.
+ */
public final class NavigationCancelledEvent extends EntityNavigationEvent {
public NavigationCancelledEvent(EntityNavigationController controller) {
super(controller);
diff --git a/src/main/java/ca/atlasengine/pathfinding/event/NavigationEvent.java b/src/main/java/ca/atlasengine/pathfinding/event/NavigationEvent.java
index ad34229..0a8e4e2 100644
--- a/src/main/java/ca/atlasengine/pathfinding/event/NavigationEvent.java
+++ b/src/main/java/ca/atlasengine/pathfinding/event/NavigationEvent.java
@@ -1,6 +1,7 @@
package ca.atlasengine.pathfinding.event;
-import ca.atlasengine.pathfinding.NavigationSystem;import net.minestom.server.event.Event;
+import ca.atlasengine.pathfinding.NavigationSystem;
+import net.minestom.server.event.Event;
/**
* Anything a {@link ca.atlasengine.pathfinding.NavigationSystem} announces.
diff --git a/src/main/java/ca/atlasengine/pathfinding/influence/BlockAvoidanceInfluence.java b/src/main/java/ca/atlasengine/pathfinding/influence/BlockAvoidanceInfluence.java
index 593d50b..b3bbd34 100644
--- a/src/main/java/ca/atlasengine/pathfinding/influence/BlockAvoidanceInfluence.java
+++ b/src/main/java/ca/atlasengine/pathfinding/influence/BlockAvoidanceInfluence.java
@@ -51,7 +51,7 @@ public InfluenceResult evaluate(
for (int y = point.blockY() - radius; y <= point.blockY() + radius; y++) {
for (int z = point.blockZ() - radius; z <= point.blockZ() + radius; z++) {
if ((reads++ & (CANCELLATION_CHECK_INTERVAL - 1)) == 0
- && (control.cancelled() || control.timedOut())) {
+ && control.interrupted()) {
return InfluenceResult.forbidden(
"avoidance_scan_interrupted");
}
diff --git a/src/main/java/ca/atlasengine/pathfinding/influence/NavigationArea.java b/src/main/java/ca/atlasengine/pathfinding/influence/NavigationArea.java
index 03b0bd2..025455e 100644
--- a/src/main/java/ca/atlasengine/pathfinding/influence/NavigationArea.java
+++ b/src/main/java/ca/atlasengine/pathfinding/influence/NavigationArea.java
@@ -3,6 +3,7 @@
import net.minestom.server.collision.BoundingBox;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
+
import static ca.atlasengine.pathfinding.internal.Points.finite;
/**
@@ -27,7 +28,6 @@ public record NavigationArea(Point minimum, Point maximum, String name) {
maximum = new Vec(maximum.x(), maximum.y(), maximum.z());
}
-
/**
* An area covering whole block cells, inclusive of both named corners in
* any order. The cells are expanded to the continuous volume they occupy,
diff --git a/src/main/java/ca/atlasengine/pathfinding/influence/NavigationZoneInfluence.java b/src/main/java/ca/atlasengine/pathfinding/influence/NavigationZoneInfluence.java
index 5babb71..76fe5db 100644
--- a/src/main/java/ca/atlasengine/pathfinding/influence/NavigationZoneInfluence.java
+++ b/src/main/java/ca/atlasengine/pathfinding/influence/NavigationZoneInfluence.java
@@ -4,6 +4,7 @@
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.instance.block.Block;
+
import static ca.atlasengine.pathfinding.internal.Points.finite;
/**
@@ -35,7 +36,6 @@ public record NavigationZoneInfluence(
maximum = new Vec(maximum.x(), maximum.y(), maximum.z());
}
-
/**
* A zone covering whole block cells, inclusive of both named corners in any
* order. The cells are expanded to the continuous volume they occupy, so
diff --git a/src/main/java/ca/atlasengine/pathfinding/influence/ReturnRadiusInfluence.java b/src/main/java/ca/atlasengine/pathfinding/influence/ReturnRadiusInfluence.java
index 19a0f63..54a1c85 100644
--- a/src/main/java/ca/atlasengine/pathfinding/influence/ReturnRadiusInfluence.java
+++ b/src/main/java/ca/atlasengine/pathfinding/influence/ReturnRadiusInfluence.java
@@ -4,6 +4,7 @@
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.instance.block.Block;
+
import static ca.atlasengine.pathfinding.internal.Points.finite;
/**
@@ -26,7 +27,6 @@ public record ReturnRadiusInfluence(
requestStart.x(), requestStart.y(), requestStart.z());
}
-
@Override
public InfluenceResult evaluate(
Block.Getter blocks, Point point, BoundingBox box) {
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/BoxCollision.java b/src/main/java/ca/atlasengine/pathfinding/internal/BoxCollision.java
new file mode 100644
index 0000000..29e7a40
--- /dev/null
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/BoxCollision.java
@@ -0,0 +1,50 @@
+package ca.atlasengine.pathfinding.internal;
+
+import ca.atlasengine.pathfinding.terrain.BlockTraversalData;
+import net.minestom.server.collision.BoundingBox;
+import net.minestom.server.coordinate.Point;
+import net.minestom.server.coordinate.Vec;
+import net.minestom.server.instance.block.Block;
+
+/**
+ * Static overlap tests between an entity box and block collision shapes.
+ * The planner and the follower both judge a resting position here, so the two
+ * can never disagree about whether a box fits.
+ */
+public final class BoxCollision {
+ private BoxCollision() {
+ }
+
+ /** Whether any block shape stands inside {@code box} at {@code position}. */
+ public static boolean intersects(Block.Getter blocks, BoundingBox box,
+ Point position) {
+ return intersects(blocks, box, position, false);
+ }
+
+ /**
+ * Whether a climber's box fits at {@code position}. Climbable blocks are
+ * held rather than collided with, so they never obstruct.
+ */
+ public static boolean climbClear(Block.Getter blocks, BoundingBox box,
+ Point position) {
+ return !intersects(blocks, box, position, true);
+ }
+
+ private static boolean intersects(Block.Getter blocks, BoundingBox box,
+ Point position, boolean skipClimbables) {
+ var cells = box.getBlocks(position);
+ while (cells.hasNext()) {
+ var cell = cells.next();
+ int x = cell.blockX();
+ int y = cell.blockY();
+ int z = cell.blockZ();
+ Block block = blocks.getBlock(x, y, z, Block.Getter.Condition.TYPE);
+ if (skipClimbables && BlockTraversalData.isClimbable(block)) continue;
+ if (block.collisionShape().intersectBox(
+ position.sub(new Vec(x, y, z)), box)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/Footprint.java b/src/main/java/ca/atlasengine/pathfinding/internal/Footprint.java
new file mode 100644
index 0000000..fb38f0d
--- /dev/null
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/Footprint.java
@@ -0,0 +1,35 @@
+package ca.atlasengine.pathfinding.internal;
+
+import net.minestom.server.collision.BoundingBox;
+
+/**
+ * How many block cells an entity box spans when anchored at a graph cell's
+ * minimum corner. Every evaluator, classifier, and waypoint projection sizes
+ * its scans by this one rule.
+ */
+public final class Footprint {
+ private Footprint() {
+ }
+
+ /** Cells covered along one axis by a box of this extent. */
+ public static int cells(double extent) {
+ return Math.max(1, (int) Math.floor(extent + 1));
+ }
+
+ public static int width(BoundingBox box) {
+ return cells(box.width());
+ }
+
+ public static int height(BoundingBox box) {
+ return cells(box.height());
+ }
+
+ public static int depth(BoundingBox box) {
+ return cells(box.depth());
+ }
+
+ /** Mean of the three extents, which separates small flyers from large. */
+ public static double meanExtent(BoundingBox box) {
+ return (box.width() + box.height() + box.depth()) / 3;
+ }
+}
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/JumpArc.java b/src/main/java/ca/atlasengine/pathfinding/internal/JumpArc.java
index bfc4ec7..88d825d 100644
--- a/src/main/java/ca/atlasengine/pathfinding/internal/JumpArc.java
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/JumpArc.java
@@ -5,10 +5,7 @@
import net.minestom.server.collision.PhysicsResult;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
-import net.minestom.server.instance.Chunk;
-import net.minestom.server.instance.Instance;
import net.minestom.server.instance.block.Block;
-import net.minestom.server.utils.chunk.ChunkCache;
import java.util.function.BooleanSupplier;
@@ -110,18 +107,6 @@ private static BoundingBox grown(BoundingBox box, Point from, Point to,
.withY(box.relativeEnd().y() + headroom));
}
- /**
- * Live counterpart reading through a chunk cache, so an arc leaving the
- * loaded area is refused rather than throwing.
- */
- public static boolean clear(Instance instance, Chunk chunk,
- BoundingBox box, Point from, Point to,
- double apexClearance) {
- return clear(new ChunkCache(instance, chunk != null
- ? chunk : instance.getChunkAt(from), Block.STONE),
- box, from, to, apexClearance, null);
- }
-
/**
* Point-sampling the arc lets the box step diagonally past a block corner
* between two samples, so a swept test is the only one the follower and
@@ -136,7 +121,7 @@ public static boolean clear(Block.Getter blocks, BoundingBox box,
// A sweep discards whatever the box already overlaps when it starts,
// and the grown box reaches above the standing one, so the takeoff
// is the one position no sweep along the arc can judge.
- if (occupied(blocks, grown, from)) return false;
+ if (BoxCollision.intersects(blocks, grown, from)) return false;
int samples = samples(from, to);
Point previous = from;
for (int sample = 1; sample <= samples; sample++) {
@@ -155,20 +140,4 @@ public static boolean clear(Block.Getter blocks, BoundingBox box,
}
return true;
}
-
- private static boolean occupied(Block.Getter blocks, BoundingBox box,
- Point position) {
- var cells = box.getBlocks(position);
- while (cells.hasNext()) {
- var cell = cells.next();
- Vec corner = new Vec(
- cell.blockX(), cell.blockY(), cell.blockZ());
- if (blocks.getBlock(cell.blockX(), cell.blockY(), cell.blockZ())
- .collisionShape()
- .intersectBox(position.sub(corner), box)) {
- return true;
- }
- }
- return false;
- }
}
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/LoadedBlocks.java b/src/main/java/ca/atlasengine/pathfinding/internal/LoadedBlocks.java
new file mode 100644
index 0000000..8dc7aad
--- /dev/null
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/LoadedBlocks.java
@@ -0,0 +1,75 @@
+package ca.atlasengine.pathfinding.internal;
+
+import ca.atlasengine.pathfinding.result.PathResult;
+import ca.atlasengine.pathfinding.result.PathStop;
+import net.minestom.server.coordinate.CoordConversion;
+import net.minestom.server.instance.Instance;
+import net.minestom.server.instance.block.Block;
+
+/**
+ * Block reads that treat unloaded terrain as solid.
+ *
+ *
{@link Instance#getBlock} throws for a chunk that is not loaded, and a
+ * search near the edge of the loaded area reaches such chunks routinely. Read
+ * through this view instead, unloaded terrain is a wall: no route enters it,
+ * and a search that would have thrown ends as an ordinary partial route.
+ */
+public final class LoadedBlocks implements Block.Getter {
+ /** What every cell of an unloaded chunk reads as. */
+ private static final Block UNLOADED = Block.STONE;
+
+ private final Instance instance;
+ /** The chunk last seen loaded, so a run of reads in it checks once. */
+ private int loadedChunkX = Integer.MIN_VALUE;
+ private int loadedChunkZ = Integer.MIN_VALUE;
+ private boolean readUnloaded;
+
+ private LoadedBlocks(Instance instance) {
+ this.instance = instance;
+ }
+
+ /**
+ * A view of {@code blocks} that never throws for unloaded terrain. Reads
+ * still go through {@link Instance#getBlock}, so an instance overriding it
+ * is honoured. The view remembers the chunk it last read, so it belongs to
+ * one thread: create one per search or per tick-thread probe. Any other
+ * getter is returned unchanged.
+ */
+ public static Block.Getter view(Block.Getter blocks) {
+ return blocks instanceof Instance instance
+ ? new LoadedBlocks(instance) : blocks;
+ }
+
+ /**
+ * {@code result} as seen through {@code view}: an exhausted frontier only
+ * proves the target unreachable when no unloaded terrain walled it in.
+ */
+ public static PathResult qualify(Block.Getter view, PathResult result) {
+ return view instanceof LoadedBlocks loaded && loaded.readUnloaded
+ && result.stop() == PathStop.FRONTIER_EXHAUSTED
+ ? result.withStop(PathStop.TERRAIN_UNLOADED) : result;
+ }
+
+ @Override
+ public Block getBlock(int x, int y, int z, Condition condition) {
+ int chunkX = CoordConversion.globalToChunk(x);
+ int chunkZ = CoordConversion.globalToChunk(z);
+ if (chunkX != loadedChunkX || chunkZ != loadedChunkZ) {
+ if (instance.getChunk(chunkX, chunkZ) == null) return unloaded();
+ loadedChunkX = chunkX;
+ loadedChunkZ = chunkZ;
+ }
+ try {
+ return instance.getBlock(x, y, z, condition);
+ } catch (NullPointerException unloadedSinceChecked) {
+ // The chunk unloaded between the check and the read.
+ loadedChunkX = Integer.MIN_VALUE;
+ return unloaded();
+ }
+ }
+
+ private Block unloaded() {
+ readUnloaded = true;
+ return UNLOADED;
+ }
+}
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/Points.java b/src/main/java/ca/atlasengine/pathfinding/internal/Points.java
index 433e487..503dc16 100644
--- a/src/main/java/ca/atlasengine/pathfinding/internal/Points.java
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/Points.java
@@ -2,6 +2,8 @@
import net.minestom.server.coordinate.Point;
+import java.util.List;
+
/**
* Coordinate validation shared by every entry point that accepts a caller
* supplied position.
@@ -22,4 +24,13 @@ public static boolean finite(Point point) {
&& Double.isFinite(point.y())
&& Double.isFinite(point.z());
}
+
+ /** Whether {@code points} is non-empty and every element is finite. */
+ public static boolean allFinite(List extends Point> points) {
+ if (points == null || points.isEmpty()) return false;
+ for (Point point : points) {
+ if (point == null || !finite(point)) return false;
+ }
+ return true;
+ }
}
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/RouteLength.java b/src/main/java/ca/atlasengine/pathfinding/internal/RouteLength.java
new file mode 100644
index 0000000..433a3d0
--- /dev/null
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/RouteLength.java
@@ -0,0 +1,33 @@
+package ca.atlasengine.pathfinding.internal;
+
+import ca.atlasengine.pathfinding.result.PathNode;
+import net.minestom.server.coordinate.Point;
+
+import java.util.List;
+
+/** Polyline lengths of a route's waypoints. */
+public final class RouteLength {
+ private RouteLength() {
+ }
+
+ /** Length walked from the first waypoint to the last. */
+ public static double of(List nodes) {
+ return nodes.isEmpty() ? 0
+ : from(nodes.getFirst().asVec(), nodes, 1);
+ }
+
+ /**
+ * Length walked from {@code start} through every waypoint from
+ * {@code fromIndex} onward.
+ */
+ public static double from(Point start, List nodes, int fromIndex) {
+ double length = 0;
+ Point cursor = start;
+ for (int index = fromIndex; index < nodes.size(); index++) {
+ Point next = nodes.get(index).asVec();
+ length += cursor.distance(next);
+ cursor = next;
+ }
+ return length;
+ }
+}
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/SearchLatency.java b/src/main/java/ca/atlasengine/pathfinding/internal/SearchLatency.java
new file mode 100644
index 0000000..b84ef6f
--- /dev/null
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/SearchLatency.java
@@ -0,0 +1,33 @@
+package ca.atlasengine.pathfinding.internal;
+
+import ca.atlasengine.pathfinding.metrics.NavigationMetrics;
+
+/** Converts observed search latency into the tick clock followers run on. */
+public final class SearchLatency {
+ private static final long MICROS_PER_TICK = 50_000;
+ /** Multiple of the observed p99 after which a search is presumed lost. */
+ private static final long STALL_TAIL_MULTIPLIER = 4;
+
+ private SearchLatency() {
+ }
+
+ /** Whole ticks spanned by a latency in microseconds. */
+ public static long ticks(long micros) {
+ return micros / MICROS_PER_TICK;
+ }
+
+ /**
+ * Whether a search that has been in flight for {@code waitedTicks} will
+ * never land. Work in flight is left alone until the tail the system
+ * actually observes says so, so a chased mob keeps receiving routes however
+ * slow searches become. The tail is read only once the floor has already
+ * elapsed, so the ordinary path costs nothing.
+ */
+ public static boolean abandoned(long waitedTicks, int minimumStallTicks,
+ NavigationMetrics metrics) {
+ if (waitedTicks < minimumStallTicks) return false;
+ long tail = ticks(metrics.snapshot().latency().p99Micros());
+ return waitedTicks >= Math.max(minimumStallTicks,
+ tail * STALL_TAIL_MULTIPLIER);
+ }
+}
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/adaptive/SharedMeshCoordinator.java b/src/main/java/ca/atlasengine/pathfinding/internal/adaptive/SharedMeshCoordinator.java
index 7f2bdd9..9624185 100644
--- a/src/main/java/ca/atlasengine/pathfinding/internal/adaptive/SharedMeshCoordinator.java
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/adaptive/SharedMeshCoordinator.java
@@ -1,20 +1,21 @@
package ca.atlasengine.pathfinding.internal.adaptive;
-import ca.atlasengine.pathfinding.adaptive.SharedMeshPolicy;
-import ca.atlasengine.pathfinding.adaptive.SharedMeshHandle;
-import ca.atlasengine.pathfinding.adaptive.SharedMeshSource;
import ca.atlasengine.pathfinding.NavigationPlan;
+import ca.atlasengine.pathfinding.adaptive.SharedMeshHandle;
+import ca.atlasengine.pathfinding.adaptive.SharedMeshPolicy;
import ca.atlasengine.pathfinding.adaptive.SharedMeshRetention;
+import ca.atlasengine.pathfinding.adaptive.SharedMeshSource;
import ca.atlasengine.pathfinding.adaptive.SharedNavigationRoute;
import ca.atlasengine.pathfinding.adaptive.SharedRouteMesh;
-import ca.atlasengine.pathfinding.search.EntityTraversalState;
+import ca.atlasengine.pathfinding.influence.NavigationInfluence;
+import ca.atlasengine.pathfinding.internal.RouteLength;
import ca.atlasengine.pathfinding.metrics.NavigationMetrics;
import ca.atlasengine.pathfinding.profile.NavigationProfile;
-import ca.atlasengine.pathfinding.search.NavigationRequest;
import ca.atlasengine.pathfinding.result.PathNode;
import ca.atlasengine.pathfinding.result.PathNodeCost;
import ca.atlasengine.pathfinding.result.PathStatus;
-import ca.atlasengine.pathfinding.influence.NavigationInfluence;
+import ca.atlasengine.pathfinding.search.EntityTraversalState;
+import ca.atlasengine.pathfinding.search.NavigationRequest;
import net.minestom.server.collision.BoundingBox;
import net.minestom.server.coordinate.Point;
import net.minestom.server.instance.block.Block;
@@ -58,11 +59,6 @@ CompletableFuture submit(
private long sequence;
private boolean closed;
- public SharedMeshCoordinator(
- PlanSubmitter submitter, SharedMeshPolicy config) {
- this(submitter, config, new NavigationMetrics());
- }
-
/** Records dispatch decisions into a recorder shared with a service. */
public SharedMeshCoordinator(
PlanSubmitter submitter, SharedMeshPolicy config,
@@ -250,13 +246,7 @@ private void applyDemotionHysteresis(
&& region.activeMembers < config.retentionRequests()
&& currentTick - region.lastRequestTick
>= config.targetIdleTicks()) {
- region.promoted = false;
- region.promoting = false;
- region.fields.clear();
- region.fieldEdges = Set.of();
- region.publishedPlans.clear();
- region.requestCount = region.activeMembers;
- region.invalidate();
+ region.demote();
} else if (graphChanged && region.certifying()) {
region.rebuild(config);
}
@@ -304,7 +294,7 @@ private static RebuildResult computeRebuild(
SharedRouteMesh.Builder builder =
SharedRouteMesh.builder(snapshot.worldRevision());
snapshot.edges().forEach((edge, plan) -> builder.route(
- edge.from, edge.to, plan, graphLength(plan.nodes())));
+ edge.from, edge.to, plan, RouteLength.of(plan.nodes())));
mesh = builder.build();
} catch (IllegalArgumentException | IllegalStateException ignored) {
// The retained edges do not form a buildable mesh. Every
@@ -575,15 +565,6 @@ private void ensureOpen() {
if (closed) throw new IllegalStateException("coordinator is closed");
}
- private static double graphLength(List nodes) {
- double length = 0;
- for (int index = 1; index < nodes.size(); index++) {
- length += nodes.get(index - 1).asVec()
- .distance(nodes.get(index).asVec());
- }
- return length;
- }
-
/**
* {@code retainedNodes} is the waypoint-mesh endpoint count. The three
* plan-map sizes are counted separately because a region retains routes
@@ -629,15 +610,24 @@ boolean certifying() {
return promoted || promoting;
}
+ /** Drops every route retained under an older world revision. */
void reset(long revision, long tick) {
worldRevision = revision;
lastRequestTick = tick;
- requestCount = activeMembers;
- promoted = false;
- promoting = false;
graph.edges.clear();
observedPlans.clear();
certifiedPlans.clear();
+ demote();
+ }
+
+ /**
+ * Stops serving shared plans and restarts the promotion count from the
+ * members still present. The retained skeleton is kept.
+ */
+ void demote() {
+ requestCount = activeMembers;
+ promoted = false;
+ promoting = false;
fields.clear();
fieldEdges = Set.of();
publishedPlans.clear();
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/adaptive/TargetCell.java b/src/main/java/ca/atlasengine/pathfinding/internal/adaptive/TargetCell.java
index 1e9a22c..a3c0087 100644
--- a/src/main/java/ca/atlasengine/pathfinding/internal/adaptive/TargetCell.java
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/adaptive/TargetCell.java
@@ -1,6 +1,7 @@
package ca.atlasengine.pathfinding.internal.adaptive;
-import ca.atlasengine.pathfinding.search.EntityPathfinder;import net.minestom.server.coordinate.Point;
+import ca.atlasengine.pathfinding.search.EntityPathfinder;
+import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
/**
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/movement/ClimbMovementExecutor.java b/src/main/java/ca/atlasengine/pathfinding/internal/movement/ClimbMovementExecutor.java
index 0b21b8e..1d3d311 100644
--- a/src/main/java/ca/atlasengine/pathfinding/internal/movement/ClimbMovementExecutor.java
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/movement/ClimbMovementExecutor.java
@@ -1,6 +1,6 @@
package ca.atlasengine.pathfinding.internal.movement;
-import ca.atlasengine.pathfinding.terrain.BlockTraversalData;
+import ca.atlasengine.pathfinding.internal.BoxCollision;
import net.minestom.server.collision.BoundingBox;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
@@ -75,20 +75,6 @@ private static boolean climbMovementClear(
*/
public static boolean movementClear(
Block.Getter blocks, BoundingBox box, Point position) {
- var iterator = box.getBlocks(position);
- while (iterator.hasNext()) {
- var mutable = iterator.next();
- Block block = blocks.getBlock(mutable.blockX(),
- mutable.blockY(), mutable.blockZ(),
- Block.Getter.Condition.TYPE);
- if (BlockTraversalData.isClimbable(block)) continue;
- Point blockPosition = new Vec(mutable.blockX(), mutable.blockY(),
- mutable.blockZ());
- if (block.collisionShape().intersectBox(
- position.sub(blockPosition), box)) {
- return false;
- }
- }
- return true;
+ return BoxCollision.climbClear(blocks, box, position);
}
}
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/movement/NavigationRequestFactory.java b/src/main/java/ca/atlasengine/pathfinding/internal/movement/NavigationRequestFactory.java
index 6f36bef..cadf1b5 100644
--- a/src/main/java/ca/atlasengine/pathfinding/internal/movement/NavigationRequestFactory.java
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/movement/NavigationRequestFactory.java
@@ -1,13 +1,14 @@
package ca.atlasengine.pathfinding.internal.movement;
-import ca.atlasengine.pathfinding.terrain.BlockTraversalData;
+import ca.atlasengine.pathfinding.influence.NavigationInfluence;
+import ca.atlasengine.pathfinding.internal.Footprint;
import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;
-import ca.atlasengine.pathfinding.search.EntityTraversalState;
import ca.atlasengine.pathfinding.profile.NavigationMode;
import ca.atlasengine.pathfinding.profile.NavigationProfile;
+import ca.atlasengine.pathfinding.search.EntityTraversalState;
import ca.atlasengine.pathfinding.search.NavigationRequest;
import ca.atlasengine.pathfinding.search.StartNodeSampler;
-import ca.atlasengine.pathfinding.influence.NavigationInfluence;
+import ca.atlasengine.pathfinding.terrain.BlockTraversalData;
import ca.atlasengine.pathfinding.terrain.MobTraversalProfile;
import net.minestom.server.collision.BoundingBox;
import net.minestom.server.coordinate.Point;
@@ -15,10 +16,8 @@
import net.minestom.server.entity.LivingEntity;
import net.minestom.server.entity.attribute.Attribute;
import net.minestom.server.instance.Instance;
-import net.minestom.server.instance.block.Block;
import java.util.List;
-import java.util.Set;
import java.util.SplittableRandom;
import java.util.random.RandomGenerator;
@@ -26,6 +25,10 @@
* Assembles search requests from the entity's live state.
*/
public final class NavigationRequestFactory {
+ private static final double DEFAULT_SEARCH_RANGE = 16;
+ /** Mean extent below which a flyer's start is sampled around it. */
+ private static final double SMALL_FLYER_EXTENT = 1;
+ private static final int SMALL_FLYER_START_SAMPLES = 10;
private final Entity entity;
private final boolean refreshBuiltinProfile;
private final RandomGenerator startNodeRandom;
@@ -41,7 +44,8 @@ public NavigationRequest build(NavigationProfile profile, Instance instance,
Point start, Point destination, BoundingBox box,
List influences,
double searchRangeMultiplier, boolean entityInWater) {
- double searchRange = liveNodeSearchRange() * searchRangeMultiplier;
+ double searchRange = searchRange(entity, refreshBuiltinProfile)
+ * searchRangeMultiplier;
return NavigationRequest.builder(instance, start, destination, box,
snapshotRailState(profile, instance, start))
.maxPathLength(searchRange)
@@ -65,8 +69,7 @@ public static RandomGenerator entityRandomOrNull(Entity entity) {
private static NavigationProfile snapshotRailState(
NavigationProfile profile, Instance instance, Point start) {
- boolean onRail = BlockTraversalData.isRail(instance.getBlock(start))
- || BlockTraversalData.isRail(instance.getBlock(start.add(0, -1, 0)));
+ boolean onRail = BlockTraversalData.isRailAt(instance, start);
MobTraversalProfile base = profile.mobProfile();
if (base.currentlyOnRail() == onRail) return profile;
MobTraversalProfile snapshot = MobTraversalProfile.builder(base.name() + "_request")
@@ -76,23 +79,24 @@ private static NavigationProfile snapshotRailState(
return profile.withMobProfile(snapshot);
}
- private double liveNodeSearchRange() {
+ /**
+ * How far the entity searches: its live follow range, raised to the
+ * baseline navigation's own minimum when it navigates as its built-in type.
+ */
+ public static double searchRange(Entity entity, boolean builtinProfile) {
double followRange = entity instanceof LivingEntity living
? living.getAttribute(Attribute.FOLLOW_RANGE).getValue()
- : 16;
- return Math.max(
- followRange,
- refreshBuiltinProfile
- ? BuiltinNavigationProfiles.requiredPathLength(entity)
- : 16);
+ : DEFAULT_SEARCH_RANGE;
+ return Math.max(followRange, builtinProfile
+ ? BuiltinNavigationProfiles.requiredPathLength(entity)
+ : DEFAULT_SEARCH_RANGE);
}
private EntityTraversalState snapshotEntityState(
NavigationProfile profile, Instance instance, boolean entityInWater) {
- Set standable = profile.mobProfile().standsOnLava()
- ? Set.of(Block.LAVA) : Set.of();
return new EntityTraversalState(
- entity.isOnGround(), entityInWater, standable,
+ entity.isOnGround(), entityInWater,
+ profile.mobProfile().standableFluids(),
instance.getCachedDimensionType().minY(),
instance.getCachedDimensionType().maxY() - 1,
startCandidates(profile));
@@ -100,12 +104,9 @@ private EntityTraversalState snapshotEntityState(
private List startCandidates(NavigationProfile profile) {
if (profile.mode() != NavigationMode.FLYING) return List.of();
- var box = entity.getBoundingBox();
- double meanSize = (box.width() + box.height() + box.depth()) / 3;
- if (meanSize >= 1) return List.of();
- return StartNodeSampler.sampleSmallEntity(
- entity.getPosition(), box, startNodeRandom, 10);
+ BoundingBox box = entity.getBoundingBox();
+ if (Footprint.meanExtent(box) >= SMALL_FLYER_EXTENT) return List.of();
+ return StartNodeSampler.sampleSmallEntity(entity.getPosition(), box,
+ startNodeRandom, SMALL_FLYER_START_SAMPLES);
}
-
-
}
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/movement/PartialRouteReplanner.java b/src/main/java/ca/atlasengine/pathfinding/internal/movement/PartialRouteReplanner.java
index 61c6112..1850643 100644
--- a/src/main/java/ca/atlasengine/pathfinding/internal/movement/PartialRouteReplanner.java
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/movement/PartialRouteReplanner.java
@@ -1,5 +1,6 @@
package ca.atlasengine.pathfinding.internal.movement;
+import ca.atlasengine.pathfinding.internal.RouteLength;
import ca.atlasengine.pathfinding.result.PathNode;
import ca.atlasengine.pathfinding.result.PathResult;
import ca.atlasengine.pathfinding.result.PathStop;
@@ -46,20 +47,6 @@ public void widen() {
searchRangeMultiplier * 2);
}
- /**
- * Chains bounded partial searches for long routes. A nearby
- * unreachable target remains terminal, and an identical partial endpoint
- * is never submitted twice, preventing autonomous retry loops.
- *
- *
For a route whose search reason is not recorded, such as one replayed
- * from a cached plan.
- */
- public Continuation continueLongPartialRoute(
- Point position, Point target, List nodes) {
- return continueLongPartialRoute(
- position, target, nodes, PathStop.NOT_SEARCHED);
- }
-
/**
* Chains bounded partial searches for long routes. A nearby
* unreachable target remains terminal, and an identical partial endpoint
@@ -117,16 +104,7 @@ public Continuation continueLongPartialRoute(
private static boolean budgetBound(
PathStop stop, List nodes, double remaining) {
return stop == PathStop.FRONTIER_TRUNCATED
- && walkedLength(nodes) > remaining;
- }
-
- private static double walkedLength(List nodes) {
- double walked = 0;
- for (int index = 1; index < nodes.size(); index++) {
- walked += nodes.get(index - 1).asVec()
- .distance(nodes.get(index).asVec());
- }
- return walked;
+ && RouteLength.of(nodes) > remaining;
}
public boolean shouldExpandVerticalDetour(PathResult path, Point target) {
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/movement/PathPostProcessor.java b/src/main/java/ca/atlasengine/pathfinding/internal/movement/PathPostProcessor.java
index 800f8c7..2c8b75b 100644
--- a/src/main/java/ca/atlasengine/pathfinding/internal/movement/PathPostProcessor.java
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/movement/PathPostProcessor.java
@@ -1,5 +1,6 @@
package ca.atlasengine.pathfinding.internal.movement;
+import ca.atlasengine.pathfinding.internal.LoadedBlocks;
import ca.atlasengine.pathfinding.profile.NavigationMode;
import ca.atlasengine.pathfinding.profile.NavigationProfile;
import ca.atlasengine.pathfinding.result.PathNode;
@@ -31,10 +32,11 @@ public static Result process(Instance instance, NavigationProfile profile,
private static List adjustCauldronNodes(
Instance instance, List path) {
if (instance == null || path.isEmpty()) return path;
+ Block.Getter blocks = LoadedBlocks.view(instance);
List adjusted = new ArrayList<>(path);
for (int i = 0; i < adjusted.size(); i++) {
PathNode node = adjusted.get(i);
- Block block = instance.getBlock(
+ Block block = blocks.getBlock(
node.graphX(), node.graphY(), node.graphZ(),
Block.Getter.Condition.TYPE);
if (!BlockTagIndex.contains("cauldrons", block)) continue;
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/movement/PathSubmitter.java b/src/main/java/ca/atlasengine/pathfinding/internal/movement/PathSubmitter.java
deleted file mode 100644
index 7fd38f8..0000000
--- a/src/main/java/ca/atlasengine/pathfinding/internal/movement/PathSubmitter.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package ca.atlasengine.pathfinding.internal.movement;
-
-import ca.atlasengine.pathfinding.profile.NavigationModifiers;
-import ca.atlasengine.pathfinding.result.PathResult;
-import ca.atlasengine.pathfinding.influence.NavigationInfluence;
-import net.minestom.server.collision.BoundingBox;
-import net.minestom.server.coordinate.Point;
-import net.minestom.server.instance.Instance;
-
-import java.util.List;
-import java.util.concurrent.CompletableFuture;
-
-/**
- * Seam between a controller and the search service it plans with.
- */
-@FunctionalInterface
-public interface PathSubmitter {
- CompletableFuture submit(
- Object key, Instance instance, Point start,
- Point target, BoundingBox boundingBox,
- List influences,
- NavigationModifiers modifiers);
-}
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/movement/PlatformJumpMovementExecutor.java b/src/main/java/ca/atlasengine/pathfinding/internal/movement/PlatformJumpMovementExecutor.java
index 7166efa..015a5d8 100644
--- a/src/main/java/ca/atlasengine/pathfinding/internal/movement/PlatformJumpMovementExecutor.java
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/movement/PlatformJumpMovementExecutor.java
@@ -1,9 +1,10 @@
package ca.atlasengine.pathfinding.internal.movement;
import ca.atlasengine.pathfinding.MovementExecutionMode;
-import ca.atlasengine.pathfinding.result.PathNode;
-import ca.atlasengine.pathfinding.profile.PlatformJumpCapabilities;
import ca.atlasengine.pathfinding.internal.JumpArc;
+import ca.atlasengine.pathfinding.internal.LoadedBlocks;
+import ca.atlasengine.pathfinding.profile.PlatformJumpCapabilities;
+import ca.atlasengine.pathfinding.result.PathNode;
import net.minestom.server.collision.CollisionUtils;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
@@ -140,7 +141,9 @@ private static boolean liveJumpArcClear(
Entity entity = context.entity();
Instance instance = entity.getInstance();
if (instance == null) return false;
- return JumpArc.clear(instance, entity.getChunk(),
- entity.getBoundingBox(), from, to, jump.apexClearance());
+ // Read as the search read it, so an arc leaving the loaded area is
+ // refused by both rather than throwing here.
+ return JumpArc.clear(LoadedBlocks.view(instance),
+ entity.getBoundingBox(), from, to, jump.apexClearance(), null);
}
}
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/movement/RouteGeometry.java b/src/main/java/ca/atlasengine/pathfinding/internal/movement/RouteGeometry.java
index eb0572c..3db1ab7 100644
--- a/src/main/java/ca/atlasengine/pathfinding/internal/movement/RouteGeometry.java
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/movement/RouteGeometry.java
@@ -1,5 +1,6 @@
package ca.atlasengine.pathfinding.internal.movement;
+import ca.atlasengine.pathfinding.internal.LoadedBlocks;
import ca.atlasengine.pathfinding.profile.NavigationMode;
import ca.atlasengine.pathfinding.result.PathNode;
import ca.atlasengine.pathfinding.terrain.BlockTraversalData;
@@ -10,7 +11,6 @@
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.Entity;
-import net.minestom.server.entity.EntityType;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.block.Block;
@@ -20,7 +20,6 @@
*/
public final class RouteGeometry {
private final MovementContext context;
- private final TerrainClassifier terrainClassifier = new TerrainClassifier();
public RouteGeometry(MovementContext context) {
this.context = context;
@@ -30,7 +29,8 @@ public boolean isClimbableWaypoint(PathNode node) {
Entity entity = context.entity();
return context.profile().groundCapabilities().climbables().enabled()
&& entity.getInstance() != null
- && BlockTraversalData.isClimbableAt(entity.getInstance(),
+ && BlockTraversalData.isClimbableAt(
+ LoadedBlocks.view(entity.getInstance()),
node.graphX(), node.graphY(), node.graphZ());
}
@@ -115,14 +115,17 @@ && navigationMode() == NavigationMode.WATER) {
} else {
Instance instance = entity.getInstance();
if (instance == null) return false;
- type = terrainClassifier.classifyAnchored(
- instance, node.graphX(), node.graphY(), node.graphZ(),
+ // The classifier reads neighbouring cells, which at the edge of
+ // the loaded area lie in chunks that are not loaded.
+ type = TerrainClassifier.classifyAnchored(
+ LoadedBlocks.view(instance),
+ node.graphX(), node.graphY(), node.graphZ(),
entity.getBoundingBox(), context.profile().mobProfile());
}
- return permitsCornerCut(entity.getEntityType(), type);
+ return permitsCornerCut(type);
}
- public static boolean permitsCornerCut(EntityType entityType, TerrainType type) {
+ public static boolean permitsCornerCut(TerrainType type) {
return type != TerrainType.FIRE_IN_NEIGHBOR
&& type != TerrainType.DAMAGING_IN_NEIGHBOR
&& type != TerrainType.WALKABLE_DOOR
@@ -191,10 +194,11 @@ private boolean canMoveDirectly(Point start, Point destination) {
if (context.profile().directPathIgnoresFluids()) return true;
// Flying navigation treats fluids as ray blockers.
+ Block.Getter blocks = LoadedBlocks.view(instance);
int samples = Math.max(1, (int) Math.ceil(displacement.length() * 20));
for (int i = 0; i <= samples; i++) {
Point point = rayStart.add(displacement.mul((double) i / samples));
- if (instance.getBlock(point, Block.Getter.Condition.TYPE).liquid()) {
+ if (blocks.getBlock(point, Block.Getter.Condition.TYPE).liquid()) {
return false;
}
}
@@ -238,7 +242,8 @@ public boolean isEntityInWater() {
Entity entity = context.entity();
Instance instance = entity.getInstance();
if (instance == null) return false;
- return volumeTouchesWater(instance, entity.getPosition(),
+ return volumeTouchesWater(LoadedBlocks.view(instance),
+ entity.getPosition(),
entity.getBoundingBox());
}
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/movement/RouteSplicer.java b/src/main/java/ca/atlasengine/pathfinding/internal/movement/RouteSplicer.java
index 759ce85..d87875d 100644
--- a/src/main/java/ca/atlasengine/pathfinding/internal/movement/RouteSplicer.java
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/movement/RouteSplicer.java
@@ -1,5 +1,6 @@
package ca.atlasengine.pathfinding.internal.movement;
+import ca.atlasengine.pathfinding.metrics.SpliceCounts;
import ca.atlasengine.pathfinding.result.PathNode;
import net.minestom.server.coordinate.Point;
@@ -44,20 +45,6 @@ public final class RouteSplicer {
public record Join(List nodes, int offset) {
}
- /**
- * Why armed splices did or did not become joined routes. {@code overrun}
- * counts the entities that passed the splice node before their plan
- * landed, {@code expired} those whose search outran its own predicted
- * wait, {@code superseded} the routes replaced while the search ran, and
- * {@code seamMismatch} the plans that did not start where they were asked
- * to. Each is a fallback to replanning from the current position, never a
- * discarded route.
- */
- public record Counts(long armed, long joined, long seamMismatch,
- long overrun, long expired, long superseded,
- long shortRoute, long divergent) {
- }
-
private final int[] observedTicks = new int[LATENCY_SAMPLES];
private int observedCursor;
private int observedCount;
@@ -222,8 +209,8 @@ public Join join(List route, int nodeIndex,
return new Join(List.copyOf(nodes), offset);
}
- public Counts counts() {
- return new Counts(armed, joined, seamMismatch, overrun, expired,
+ public SpliceCounts counts() {
+ return new SpliceCounts(armed, joined, seamMismatch, overrun, expired,
superseded, shortRoute, divergent);
}
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/search/AmphibiousNodeEvaluator.java b/src/main/java/ca/atlasengine/pathfinding/internal/search/AmphibiousNodeEvaluator.java
index 10520c1..d917896 100644
--- a/src/main/java/ca/atlasengine/pathfinding/internal/search/AmphibiousNodeEvaluator.java
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/search/AmphibiousNodeEvaluator.java
@@ -1,8 +1,8 @@
package ca.atlasengine.pathfinding.internal.search;
+import ca.atlasengine.pathfinding.influence.NavigationInfluence;
import ca.atlasengine.pathfinding.search.EntityTraversalState;
import ca.atlasengine.pathfinding.search.SearchControl;
-import ca.atlasengine.pathfinding.influence.NavigationInfluence;
import ca.atlasengine.pathfinding.terrain.BlockTagIndex;
import ca.atlasengine.pathfinding.terrain.MobTraversalProfile;
import ca.atlasengine.pathfinding.terrain.TerrainType;
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/search/ClearanceProbe.java b/src/main/java/ca/atlasengine/pathfinding/internal/search/ClearanceProbe.java
index 3d02b9b..ff0b782 100644
--- a/src/main/java/ca/atlasengine/pathfinding/internal/search/ClearanceProbe.java
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/search/ClearanceProbe.java
@@ -1,5 +1,6 @@
package ca.atlasengine.pathfinding.internal.search;
+import ca.atlasengine.pathfinding.internal.Footprint;
import ca.atlasengine.pathfinding.terrain.BlockTraversalData;
import ca.atlasengine.pathfinding.terrain.TerrainType;
import net.minestom.server.collision.BoundingBox;
@@ -72,8 +73,7 @@ boolean reachableWithoutCollision(SearchNode target) {
double dy = target.y - startPosition.y() + box.height() / 2;
double dz = target.z - startPosition.z() + box.depth() / 2;
double length = Math.sqrt(dx * dx + dy * dy + dz * dz);
- double averageSize = (box.width() + box.height() + box.depth()) / 3;
- int steps = (int) Math.ceil(length / averageSize);
+ int steps = (int) Math.ceil(length / Footprint.meanExtent(box));
if (steps <= 0) return true;
Vec delta = new Vec(dx / steps, dy / steps, dz / steps);
Point position = startPosition;
@@ -102,8 +102,8 @@ boolean swungPanelBlocks(SearchNode from, SearchNode to) {
private boolean sweepHitsPanel(SearchNode from, SearchNode to) {
double halfWidth = box.width() / 2;
double halfDepth = box.depth() / 2;
- double centerX = GroundNodeEvaluator.footprintWidth(box) * 0.5;
- double centerZ = GroundNodeEvaluator.footprintDepth(box) * 0.5;
+ double centerX = Footprint.width(box) * 0.5;
+ double centerZ = Footprint.depth(box) * 0.5;
double minX = Math.min(from.x, to.x) + centerX - halfWidth;
double maxX = Math.max(from.x, to.x) + centerX + halfWidth;
double minZ = Math.min(from.z, to.z) + centerZ - halfDepth;
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/search/ClimbSupport.java b/src/main/java/ca/atlasengine/pathfinding/internal/search/ClimbSupport.java
index 89f1a83..dd9e7e0 100644
--- a/src/main/java/ca/atlasengine/pathfinding/internal/search/ClimbSupport.java
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/search/ClimbSupport.java
@@ -1,9 +1,11 @@
package ca.atlasengine.pathfinding.internal.search;
-import ca.atlasengine.pathfinding.profile.ClimbableCapabilities;
-import ca.atlasengine.pathfinding.result.PathNode;
import ca.atlasengine.pathfinding.influence.InfluenceResult;
import ca.atlasengine.pathfinding.influence.NavigationInfluence;
+import ca.atlasengine.pathfinding.internal.BoxCollision;
+import ca.atlasengine.pathfinding.internal.Footprint;
+import ca.atlasengine.pathfinding.profile.ClimbableCapabilities;
+import ca.atlasengine.pathfinding.result.PathNode;
import ca.atlasengine.pathfinding.terrain.BlockTraversalData;
import ca.atlasengine.pathfinding.terrain.TerrainType;
import net.minestom.server.collision.BoundingBox;
@@ -121,14 +123,15 @@ private Point waypoint(int x, int y, int z) {
BoundingBox box = evaluator.box;
return new Vec(
waypointX(x, y, z,
- GroundNodeEvaluator.footprintWidth(box) * 0.5),
+ Footprint.width(box) * 0.5),
evaluator.waypointY(x, y, z),
waypointZ(x, y, z,
- GroundNodeEvaluator.footprintDepth(box) * 0.5));
+ Footprint.depth(box) * 0.5));
}
/**
- * Judges one climb node against the world the follower will sweep at it.
+ * Judges one climb node against the world the follower will sweep at it,
+ * by the follower's own climb rule.
* The follower opens the cells its box reaches as it drives to the
* waypoint, so those are read in the states it opens them into. No other
* cell is: nothing opens a block the mob never stands over, and reading
@@ -138,19 +141,7 @@ private boolean occupiedWhileClimbing(Point position) {
BoundingBox box = evaluator.box;
Block.Getter blocks = evaluator.profile.openedFootprintView(
evaluator.blocks, position, box);
- var iterator = box.getBlocks(position);
- while (iterator.hasNext()) {
- var mutable = iterator.next();
- Block block = blocks.getBlock(mutable.blockX(),
- mutable.blockY(), mutable.blockZ(),
- Block.Getter.Condition.TYPE);
- if (BlockTraversalData.isClimbable(block)) continue;
- Point blockPosition = new Vec(mutable.blockX(),
- mutable.blockY(), mutable.blockZ());
- if (block.collisionShape().intersectBox(
- position.sub(blockPosition), box)) return true;
- }
- return false;
+ return !BoxCollision.climbClear(blocks, box, position);
}
boolean anchoredAt(int x, int y, int z) {
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/search/FlyNodeEvaluator.java b/src/main/java/ca/atlasengine/pathfinding/internal/search/FlyNodeEvaluator.java
index 7f0cbdf..b269763 100644
--- a/src/main/java/ca/atlasengine/pathfinding/internal/search/FlyNodeEvaluator.java
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/search/FlyNodeEvaluator.java
@@ -1,7 +1,8 @@
package ca.atlasengine.pathfinding.internal.search;
-import ca.atlasengine.pathfinding.search.NavigationRequest;
+import ca.atlasengine.pathfinding.internal.Footprint;
import ca.atlasengine.pathfinding.result.PathNode;
+import ca.atlasengine.pathfinding.search.NavigationRequest;
import ca.atlasengine.pathfinding.search.SearchControl;
import ca.atlasengine.pathfinding.terrain.MobTraversalProfile;
import ca.atlasengine.pathfinding.terrain.TerrainClassification;
@@ -12,6 +13,7 @@
import net.minestom.server.coordinate.Vec;
import net.minestom.server.instance.block.Block;
+import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
@@ -20,16 +22,12 @@
*/
public final class FlyNodeEvaluator extends SpatialNodeEvaluator {
private static final int[][] FLY_DIRECTIONS = buildFlyDirections();
- private final List neighbors = new java.util.ArrayList<>(26);
+ private final List neighbors = new ArrayList<>(26);
public FlyNodeEvaluator(NavigationRequest request, SearchControl control) {
super(request, control);
}
- FlyNodeEvaluator(NavigationRequest request) {
- this(request, SearchControl.NONE);
- }
-
@Override
SearchNode getStart() {
Point start = startPoint();
@@ -124,12 +122,9 @@ TerrainType flyVolumeType(Point point) {
int minX = point.blockX();
int minY = point.blockY();
int minZ = point.blockZ();
- int sizeX = Math.max(1,
- (int) Math.floor(request.boundingBox().width() + 1));
- int sizeY = Math.max(1,
- (int) Math.floor(request.boundingBox().height() + 1));
- int sizeZ = Math.max(1,
- (int) Math.floor(request.boundingBox().depth() + 1));
+ int sizeX = Footprint.width(request.boundingBox());
+ int sizeY = Footprint.height(request.boundingBox());
+ int sizeZ = Footprint.depth(request.boundingBox());
MobTraversalProfile profile = mobProfile;
TerrainClassification classification = profile.classification();
EnumSet types = EnumSet.noneOf(TerrainType.class);
@@ -138,7 +133,7 @@ TerrainType flyVolumeType(Point point) {
for (int y = minY; y < minY + sizeY; y++) {
for (int z = minZ; z < minZ + sizeZ; z++) {
if (control.interruptible() && (scanned++ & 63) == 0
- && (control.cancelled() || control.timedOut())) {
+ && control.interrupted()) {
return TerrainType.BLOCKED;
}
types.add(TerrainClassifier.transform(
@@ -205,8 +200,7 @@ private Point startPoint() {
if (flyMalus(nominal) >= 0) return nominal;
BoundingBox box = request.boundingBox();
- double averageSize = (box.width() + box.height() + box.depth()) / 3;
- if (averageSize >= 1) {
+ if (Footprint.meanExtent(box) >= 1) {
double minX = point.x() + box.relativeStart().x();
double maxX = point.x() + box.relativeEnd().x();
double minZ = point.z() + box.relativeStart().z();
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/search/GroundNodeEvaluator.java b/src/main/java/ca/atlasengine/pathfinding/internal/search/GroundNodeEvaluator.java
index 06dd368..e5fe307 100644
--- a/src/main/java/ca/atlasengine/pathfinding/internal/search/GroundNodeEvaluator.java
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/search/GroundNodeEvaluator.java
@@ -1,12 +1,13 @@
package ca.atlasengine.pathfinding.internal.search;
-import ca.atlasengine.pathfinding.search.EntityTraversalState;
+import ca.atlasengine.pathfinding.influence.InfluenceResult;
+import ca.atlasengine.pathfinding.influence.NavigationInfluence;
+import ca.atlasengine.pathfinding.internal.Footprint;
import ca.atlasengine.pathfinding.profile.GroundCapabilities;
import ca.atlasengine.pathfinding.result.PathResult;
import ca.atlasengine.pathfinding.result.PathStatus;
+import ca.atlasengine.pathfinding.search.EntityTraversalState;
import ca.atlasengine.pathfinding.search.SearchControl;
-import ca.atlasengine.pathfinding.influence.InfluenceResult;
-import ca.atlasengine.pathfinding.influence.NavigationInfluence;
import ca.atlasengine.pathfinding.terrain.BlockTraversalData;
import ca.atlasengine.pathfinding.terrain.MobTraversalProfile;
import ca.atlasengine.pathfinding.terrain.TerrainClassifier;
@@ -62,7 +63,6 @@ class GroundNodeEvaluator extends NodeEvaluator {
final int minY;
final int maxY;
- private final TerrainClassifier classifier = new TerrainClassifier();
private final CoordinateNodeMap nodes = new CoordinateNodeMap();
private final List neighbors = new ArrayList<>(16);
private final SearchNode[] cardinalNeighbors =
@@ -307,8 +307,8 @@ SearchNode accepted(int x, int y, int z) {
if (malus >= 0 && !influences.isEmpty()) {
boolean influenceBlocked = false;
Point projected = new Vec(
- x + footprintWidth(box) * 0.5, floorLevel(x, y, z),
- z + footprintDepth(box) * 0.5);
+ x + Footprint.width(box) * 0.5, floorLevel(x, y, z),
+ z + Footprint.depth(box) * 0.5);
for (NavigationInfluence influence : influences) {
InfluenceResult result = influence.evaluate(
blocks, projected, box, control);
@@ -344,12 +344,12 @@ TerrainType classify(int x, int y, int z) {
|| y + box.relativeEnd().y() > maxY + 1.0) {
return TerrainType.BLOCKED;
}
- return classifier.classifyAnchored(
+ return TerrainClassifier.classifyAnchored(
blocks, x, y, z, box, profile, control);
}
TerrainType classifyAmphibious(int x, int y, int z) {
- return classifier.classifyAnchoredAmphibious(
+ return TerrainClassifier.classifyAnchoredAmphibious(
blocks, x, y, z, box, profile, control);
}
@@ -364,8 +364,8 @@ TerrainType classifyAmphibious(int x, int y, int z) {
/** Highest supporting surface anywhere under the complete graph footprint. */
double footprintFloorLevel(int x, int y, int z) {
double highest = Double.NEGATIVE_INFINITY;
- int width = footprintWidth(box);
- int depth = footprintDepth(box);
+ int width = Footprint.width(box);
+ int depth = Footprint.depth(box);
for (int offsetX = 0; offsetX < width; offsetX++) {
for (int offsetZ = 0; offsetZ < depth; offsetZ++) {
highest = Math.max(highest,
@@ -398,7 +398,7 @@ && isWater(blocks.getBlock(
}
boolean interrupted() {
- return control.cancelled() || control.timedOut();
+ return control.interrupted();
}
@Override
@@ -407,14 +407,6 @@ PathResult result(PathStatus status, SearchNode end,
return routes.assemble(status, end, visited, examined);
}
- static int footprintWidth(BoundingBox box) {
- return Math.max(1, (int) Math.floor(box.width() + 1));
- }
-
- static int footprintDepth(BoundingBox box) {
- return Math.max(1, (int) Math.floor(box.depth() + 1));
- }
-
private static double distance(SearchNode a, SearchNode b) {
return Math.sqrt((double) (a.x - b.x) * (a.x - b.x)
+ (double) (a.y - b.y) * (a.y - b.y)
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/search/GroundNodePathfinder.java b/src/main/java/ca/atlasengine/pathfinding/internal/search/GroundNodePathfinder.java
index 6937d5a..2cd5398 100644
--- a/src/main/java/ca/atlasengine/pathfinding/internal/search/GroundNodePathfinder.java
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/search/GroundNodePathfinder.java
@@ -1,9 +1,10 @@
package ca.atlasengine.pathfinding.internal.search;
-import ca.atlasengine.pathfinding.search.EntityTraversalState;
+import ca.atlasengine.pathfinding.influence.NavigationInfluence;
+import ca.atlasengine.pathfinding.internal.Points;
import ca.atlasengine.pathfinding.result.PathResult;
+import ca.atlasengine.pathfinding.search.EntityTraversalState;
import ca.atlasengine.pathfinding.search.SearchControl;
-import ca.atlasengine.pathfinding.influence.NavigationInfluence;
import ca.atlasengine.pathfinding.terrain.MobTraversalProfile;
import ca.atlasengine.pathfinding.terrain.TerrainType;
import net.minestom.server.collision.BoundingBox;
@@ -12,7 +13,6 @@
import net.minestom.server.instance.block.Block;
import java.util.List;
-import java.util.Objects;
/**
* Ground and amphibious entry points into the shared A* driver.
@@ -24,9 +24,7 @@ public PathResult findAmphibiousPathToAny(
GroundSearchSpec spec, SearchControl control,
List influences,
EntityTraversalState entityState) {
- if (invalid(targets)) {
- return PathResult.INVALID_REQUEST;
- }
+ if (!Points.allFinite(targets)) return PathResult.INVALID_REQUEST;
MobTraversalProfile searchProfile = MobTraversalProfile.builder(
profile.name() + "_amphibious_search")
.from(profile)
@@ -51,18 +49,11 @@ public PathResult findPathToAny(
GroundSearchSpec spec, SearchControl control,
List influences,
EntityTraversalState entityState) {
- if (invalid(targets)) {
- return PathResult.INVALID_REQUEST;
- }
+ if (!Points.allFinite(targets)) return PathResult.INVALID_REQUEST;
return PathSearch.find(
new GroundNodeEvaluator(blocks, start, box, profile,
influences, entityState, spec, control),
targets, spec.maxPathLength(), spec.reachRange(),
spec.maxVisited(), control);
}
-
- private static boolean invalid(List extends Point> targets) {
- return targets == null || targets.isEmpty()
- || targets.stream().anyMatch(Objects::isNull);
- }
}
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/search/PlatformJumpSupport.java b/src/main/java/ca/atlasengine/pathfinding/internal/search/PlatformJumpSupport.java
index 871bdfc..e07e8c4 100644
--- a/src/main/java/ca/atlasengine/pathfinding/internal/search/PlatformJumpSupport.java
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/search/PlatformJumpSupport.java
@@ -1,10 +1,9 @@
package ca.atlasengine.pathfinding.internal.search;
-import ca.atlasengine.pathfinding.profile.PlatformJumpCapabilities;
+import ca.atlasengine.pathfinding.internal.Footprint;
import ca.atlasengine.pathfinding.internal.JumpArc;
-import ca.atlasengine.pathfinding.terrain.BlockManipulationCapabilities;
+import ca.atlasengine.pathfinding.profile.PlatformJumpCapabilities;
import ca.atlasengine.pathfinding.terrain.BlockTraversalData;
-import ca.atlasengine.pathfinding.terrain.MobTraversalProfile;
import ca.atlasengine.pathfinding.terrain.TerrainType;
import net.minestom.server.collision.BoundingBox;
import net.minestom.server.coordinate.Point;
@@ -38,7 +37,7 @@ void addNeighbors(SearchNode current, List result) {
if (!capabilities.enabled() || evaluator.isAmphibious()
|| current.type == TerrainType.WATER
|| current.type == TerrainType.LAVA
- || !fullSupport(current)) {
+ || !fullSupport(current.x, current.y, current.z, false)) {
return;
}
int maximumDistance = (int) Math.floor(
@@ -105,17 +104,9 @@ private boolean unwalkable(int x, int y, int z) {
if (type == TerrainType.OPEN
|| evaluator.profile.malus(type) < 0) return true;
BoundingBox box = evaluator.box;
- return (GroundNodeEvaluator.footprintWidth(box) > 1
- || GroundNodeEvaluator.footprintDepth(box) > 1)
- && !fullSupport(x, y, z);
- }
-
- private boolean fullSupport(SearchNode node) {
- return fullSupport(node.x, node.y, node.z, false);
- }
-
- private boolean fullSupport(int nodeX, int nodeY, int nodeZ) {
- return fullSupport(nodeX, nodeY, nodeZ, false);
+ return (Footprint.width(box) > 1
+ || Footprint.depth(box) > 1)
+ && !fullSupport(x, y, z, false);
}
/**
@@ -125,8 +116,8 @@ private boolean fullSupport(int nodeX, int nodeY, int nodeZ) {
private boolean fullSupport(int nodeX, int nodeY, int nodeZ,
boolean opened) {
BoundingBox box = evaluator.box;
- int width = GroundNodeEvaluator.footprintWidth(box);
- int depth = GroundNodeEvaluator.footprintDepth(box);
+ int width = Footprint.width(box);
+ int depth = Footprint.depth(box);
for (int x = nodeX; x < nodeX + width; x++) {
for (int z = nodeZ; z < nodeZ + depth; z++) {
Block bodyCell = evaluator.blocks.getBlock(
@@ -199,9 +190,9 @@ private boolean arcClear(SearchNode start, SearchNode landing, Point to) {
private Point waypoint(SearchNode node) {
BoundingBox box = evaluator.box;
return new Vec(
- node.x + GroundNodeEvaluator.footprintWidth(box) * 0.5,
+ node.x + Footprint.width(box) * 0.5,
evaluator.waypointY(node.x, node.y, node.z),
- node.z + GroundNodeEvaluator.footprintDepth(box) * 0.5);
+ node.z + Footprint.depth(box) * 0.5);
}
/**
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/search/RouteAssembler.java b/src/main/java/ca/atlasengine/pathfinding/internal/search/RouteAssembler.java
index 0951be7..2621e8f 100644
--- a/src/main/java/ca/atlasengine/pathfinding/internal/search/RouteAssembler.java
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/search/RouteAssembler.java
@@ -1,5 +1,6 @@
package ca.atlasengine.pathfinding.internal.search;
+import ca.atlasengine.pathfinding.internal.Footprint;
import ca.atlasengine.pathfinding.result.PathNode;
import ca.atlasengine.pathfinding.result.PathResult;
import ca.atlasengine.pathfinding.result.PathStatus;
@@ -48,8 +49,8 @@ PathResult assemble(PathStatus status, SearchNode end,
/** Graph cells to movement waypoints, goal-first. */
private List project(SearchNode end) {
List waypoints = new ArrayList<>();
- double halfWidth = GroundNodeEvaluator.footprintWidth(box) * 0.5;
- double halfDepth = GroundNodeEvaluator.footprintDepth(box) * 0.5;
+ double halfWidth = Footprint.width(box) * 0.5;
+ double halfDepth = Footprint.depth(box) * 0.5;
for (SearchNode cursor = end; cursor != null; cursor = cursor.previous) {
waypoints.add(new PathNode(
climb.waypointX(cursor.x, cursor.y, cursor.z, halfWidth),
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/search/SearchNode.java b/src/main/java/ca/atlasengine/pathfinding/internal/search/SearchNode.java
index b0234f2..adbd6c1 100644
--- a/src/main/java/ca/atlasengine/pathfinding/internal/search/SearchNode.java
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/search/SearchNode.java
@@ -40,5 +40,4 @@ final class SearchNode {
boolean inOpenSet() {
return frontierSlot >= 0;
}
-
}
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/search/SpatialNodeEvaluator.java b/src/main/java/ca/atlasengine/pathfinding/internal/search/SpatialNodeEvaluator.java
index 2c1f9a1..4228e51 100644
--- a/src/main/java/ca/atlasengine/pathfinding/internal/search/SpatialNodeEvaluator.java
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/search/SpatialNodeEvaluator.java
@@ -1,14 +1,15 @@
package ca.atlasengine.pathfinding.internal.search;
-import ca.atlasengine.pathfinding.search.NavigationRequest;
+import ca.atlasengine.pathfinding.influence.InfluenceResult;
+import ca.atlasengine.pathfinding.influence.NavigationInfluence;
+import ca.atlasengine.pathfinding.internal.Footprint;
import ca.atlasengine.pathfinding.result.PathNode;
import ca.atlasengine.pathfinding.result.PathResult;
import ca.atlasengine.pathfinding.result.PathStatus;
+import ca.atlasengine.pathfinding.search.NavigationRequest;
import ca.atlasengine.pathfinding.search.SearchControl;
-import ca.atlasengine.pathfinding.influence.InfluenceResult;
-import ca.atlasengine.pathfinding.influence.NavigationInfluence;
import ca.atlasengine.pathfinding.terrain.MobTraversalProfile;
-import ca.atlasengine.pathfinding.terrain.TerrainType;
+import net.minestom.server.collision.BoundingBox;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
@@ -123,12 +124,10 @@ final PathResult result(PathStatus status, SearchNode end,
}
static Point movementPoint(NavigationRequest request, Point graphPoint) {
- int width = Math.max(1,
- (int) Math.floor(request.boundingBox().width() + 1));
- int depth = Math.max(1,
- (int) Math.floor(request.boundingBox().depth() + 1));
- return new Vec(graphPoint.blockX() + width * 0.5,
- graphPoint.blockY(), graphPoint.blockZ() + depth * 0.5);
+ BoundingBox box = request.boundingBox();
+ return new Vec(graphPoint.blockX() + Footprint.width(box) * 0.5,
+ graphPoint.blockY(),
+ graphPoint.blockZ() + Footprint.depth(box) * 0.5);
}
private static float distance(SearchNode a, SearchNode b) {
diff --git a/src/main/java/ca/atlasengine/pathfinding/internal/search/SwimNodeEvaluator.java b/src/main/java/ca/atlasengine/pathfinding/internal/search/SwimNodeEvaluator.java
index 727568c..bc1e653 100644
--- a/src/main/java/ca/atlasengine/pathfinding/internal/search/SwimNodeEvaluator.java
+++ b/src/main/java/ca/atlasengine/pathfinding/internal/search/SwimNodeEvaluator.java
@@ -1,7 +1,8 @@
package ca.atlasengine.pathfinding.internal.search;
-import ca.atlasengine.pathfinding.search.NavigationRequest;
+import ca.atlasengine.pathfinding.internal.Footprint;
import ca.atlasengine.pathfinding.result.PathNode;
+import ca.atlasengine.pathfinding.search.NavigationRequest;
import ca.atlasengine.pathfinding.search.SearchControl;
import ca.atlasengine.pathfinding.terrain.BlockTraversalData;
import ca.atlasengine.pathfinding.terrain.TerrainClassification;
@@ -11,6 +12,7 @@
import net.minestom.server.coordinate.Vec;
import net.minestom.server.instance.block.Block;
+import java.util.ArrayList;
import java.util.List;
/**
@@ -20,17 +22,13 @@ public final class SwimNodeEvaluator extends SpatialNodeEvaluator {
private static final int[][] SWIM_DIRECTIONS = buildSwimDirections();
private static final int[][] SWIM_DIAGONAL_GATES =
{{2, 5}, {3, 5}, {3, 4}, {2, 4}};
- private final List neighbors = new java.util.ArrayList<>(10);
+ private final List neighbors = new ArrayList<>(10);
private final SearchNode[] axialNeighbors = new SearchNode[6];
public SwimNodeEvaluator(NavigationRequest request, SearchControl control) {
super(request, control);
}
- SwimNodeEvaluator(NavigationRequest request) {
- this(request, SearchControl.NONE);
- }
-
@Override
SearchNode getStart() {
return node(startPoint(request));
@@ -103,9 +101,9 @@ TerrainType swimType(Point point) {
Block.Getter blocks = request.blocks();
BoundingBox box = request.boundingBox();
TerrainClassification classification = mobProfile.classification();
- int sizeX = Math.max(1, (int) Math.floor(box.width() + 1));
- int sizeY = Math.max(1, (int) Math.floor(box.height() + 1));
- int sizeZ = Math.max(1, (int) Math.floor(box.depth() + 1));
+ int sizeX = Footprint.width(box);
+ int sizeY = Footprint.height(box);
+ int sizeZ = Footprint.depth(box);
int minX = point.blockX();
int minY = point.blockY();
int minZ = point.blockZ();
@@ -116,7 +114,7 @@ TerrainType swimType(Point point) {
for (int y = minY; y < minY + sizeY; y++) {
for (int z = minZ; z < minZ + sizeZ; z++) {
if (control.interruptible() && (scanned++ & 63) == 0
- && (control.cancelled() || control.timedOut())) {
+ && control.interrupted()) {
return TerrainType.BLOCKED;
}
Block block = blocks.getBlock(
diff --git a/src/main/java/ca/atlasengine/pathfinding/metrics/NavigationMetrics.java b/src/main/java/ca/atlasengine/pathfinding/metrics/NavigationMetrics.java
index 178d106..d344a64 100644
--- a/src/main/java/ca/atlasengine/pathfinding/metrics/NavigationMetrics.java
+++ b/src/main/java/ca/atlasengine/pathfinding/metrics/NavigationMetrics.java
@@ -1,6 +1,7 @@
package ca.atlasengine.pathfinding.metrics;
-import ca.atlasengine.pathfinding.NavigationState;import ca.atlasengine.pathfinding.adaptive.SharedMeshSource;
+import ca.atlasengine.pathfinding.NavigationState;
+import ca.atlasengine.pathfinding.adaptive.SharedMeshSource;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
diff --git a/src/main/java/ca/atlasengine/pathfinding/metrics/SpliceCounts.java b/src/main/java/ca/atlasengine/pathfinding/metrics/SpliceCounts.java
new file mode 100644
index 0000000..7cf19c5
--- /dev/null
+++ b/src/main/java/ca/atlasengine/pathfinding/metrics/SpliceCounts.java
@@ -0,0 +1,20 @@
+package ca.atlasengine.pathfinding.metrics;
+
+/**
+ * Why one follower's armed route splices did or did not become joined routes.
+ *
+ *
A splice plans a replacement route from where the entity will be when
+ * the search lands, then joins it to the route still being walked.
+ * {@code overrun} counts the entities that passed the splice node before their
+ * plan landed, {@code expired} those whose search outran its own predicted
+ * wait, {@code superseded} the routes replaced while the search ran,
+ * {@code seamMismatch} the plans that did not start where they were asked to,
+ * and {@code shortRoute} and {@code divergent} the replans that never armed
+ * because the route was too short or headed away from the new target. Each is
+ * a fallback to replanning from the current position, never a discarded
+ * route.
+ */
+public record SpliceCounts(long armed, long joined, long seamMismatch,
+ long overrun, long expired, long superseded,
+ long shortRoute, long divergent) {
+}
diff --git a/src/main/java/ca/atlasengine/pathfinding/profile/BuiltinNavigationProfiles.java b/src/main/java/ca/atlasengine/pathfinding/profile/BuiltinNavigationProfiles.java
index f0268a7..bebdc78 100644
--- a/src/main/java/ca/atlasengine/pathfinding/profile/BuiltinNavigationProfiles.java
+++ b/src/main/java/ca/atlasengine/pathfinding/profile/BuiltinNavigationProfiles.java
@@ -3,11 +3,11 @@
import ca.atlasengine.pathfinding.terrain.BuiltinMobProfiles;
import ca.atlasengine.pathfinding.terrain.MobTraversalProfile;
import ca.atlasengine.pathfinding.terrain.TerrainType;
+import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityType;
+import net.minestom.server.entity.metadata.AgeableMobMeta;
import java.util.Map;
-import net.minestom.server.entity.Entity;
-import net.minestom.server.entity.metadata.AgeableMobMeta;
/**
* Selects an evaluator and behavior profile for baseline entity families.
diff --git a/src/main/java/ca/atlasengine/pathfinding/profile/GroundCapabilities.java b/src/main/java/ca/atlasengine/pathfinding/profile/GroundCapabilities.java
index 69f27b8..6b22a4e 100644
--- a/src/main/java/ca/atlasengine/pathfinding/profile/GroundCapabilities.java
+++ b/src/main/java/ca/atlasengine/pathfinding/profile/GroundCapabilities.java
@@ -76,20 +76,27 @@ public ClimbableCapabilities climbables() {
}
public GroundCapabilities withHorizontalEdgeCost(boolean value) {
- return new GroundCapabilities(maxStepHeight, maxFallDistance,
- allowDiagonal, value, platformJump, climbables);
+ return toBuilder().horizontalEdgeCost(value).build();
}
public GroundCapabilities withPlatformJump(
PlatformJumpCapabilities jump) {
- return new GroundCapabilities(maxStepHeight, maxFallDistance,
- allowDiagonal, horizontalEdgeCost, jump, climbables);
+ return toBuilder().platformJump(jump).build();
}
public GroundCapabilities withClimbables(
ClimbableCapabilities capabilities) {
- return new GroundCapabilities(maxStepHeight, maxFallDistance,
- allowDiagonal, horizontalEdgeCost, platformJump, capabilities);
+ return toBuilder().climbables(capabilities).build();
+ }
+
+ private Builder toBuilder() {
+ return builder()
+ .maxStepHeight(maxStepHeight)
+ .maxFallDistance(maxFallDistance)
+ .allowDiagonal(allowDiagonal)
+ .horizontalEdgeCost(horizontalEdgeCost)
+ .platformJump(platformJump)
+ .climbables(climbables);
}
@Override
diff --git a/src/main/java/ca/atlasengine/pathfinding/profile/NavigationProfile.java b/src/main/java/ca/atlasengine/pathfinding/profile/NavigationProfile.java
index ba56afd..8960219 100644
--- a/src/main/java/ca/atlasengine/pathfinding/profile/NavigationProfile.java
+++ b/src/main/java/ca/atlasengine/pathfinding/profile/NavigationProfile.java
@@ -91,39 +91,38 @@ public boolean pathToTargetsBelowSurface() {
}
public NavigationProfile withNavigationType(NavigationMode type) {
- return new NavigationProfile(type, mobProfile,
- groundCapabilities, allowBreaching,
- prefersShallowWater, avoidSun,
- directPathIgnoresFluids, pathToTargetsBelowSurface);
+ return toBuilder(type, mobProfile, groundCapabilities).build();
}
public NavigationProfile withMobProfile(MobTraversalProfile profile) {
- return new NavigationProfile(mode, profile,
- groundCapabilities, allowBreaching,
- prefersShallowWater, avoidSun,
- directPathIgnoresFluids, pathToTargetsBelowSurface);
+ return toBuilder(mode, profile, groundCapabilities).build();
}
public NavigationProfile withGroundCapabilities(
GroundCapabilities capabilities) {
- return new NavigationProfile(mode, mobProfile,
- capabilities, allowBreaching,
- prefersShallowWater, avoidSun,
- directPathIgnoresFluids, pathToTargetsBelowSurface);
+ return toBuilder(mode, mobProfile, capabilities).build();
}
public NavigationProfile withAvoidSun(boolean value) {
- return new NavigationProfile(mode, mobProfile,
- groundCapabilities, allowBreaching,
- prefersShallowWater, value,
- directPathIgnoresFluids, pathToTargetsBelowSurface);
+ return toBuilder(mode, mobProfile, groundCapabilities)
+ .avoidSun(value).build();
}
public NavigationProfile withPathToTargetsBelowSurface(boolean value) {
- return new NavigationProfile(mode, mobProfile,
- groundCapabilities, allowBreaching,
- prefersShallowWater, avoidSun,
- directPathIgnoresFluids, value);
+ return toBuilder(mode, mobProfile, groundCapabilities)
+ .pathToTargetsBelowSurface(value).build();
+ }
+
+ /** This profile's flags over the given required pieces. */
+ private Builder toBuilder(NavigationMode mode,
+ MobTraversalProfile mobProfile,
+ GroundCapabilities groundCapabilities) {
+ return new Builder(mode, mobProfile, groundCapabilities)
+ .allowBreaching(allowBreaching)
+ .prefersShallowWater(prefersShallowWater)
+ .avoidSun(avoidSun)
+ .directPathIgnoresFluids(directPathIgnoresFluids)
+ .pathToTargetsBelowSurface(pathToTargetsBelowSurface);
}
@Override
@@ -212,9 +211,9 @@ public Builder pathToTargetsBelowSurface(boolean value) {
public NavigationProfile build() {
return new NavigationProfile(mode, mobProfile,
- groundCapabilities, allowBreaching,
- prefersShallowWater, avoidSun,
- directPathIgnoresFluids, pathToTargetsBelowSurface);
+ groundCapabilities, allowBreaching,
+ prefersShallowWater, avoidSun,
+ directPathIgnoresFluids, pathToTargetsBelowSurface);
}
}
}
diff --git a/src/main/java/ca/atlasengine/pathfinding/result/PathNode.java b/src/main/java/ca/atlasengine/pathfinding/result/PathNode.java
index b5fddd7..6785ad6 100644
--- a/src/main/java/ca/atlasengine/pathfinding/result/PathNode.java
+++ b/src/main/java/ca/atlasengine/pathfinding/result/PathNode.java
@@ -32,14 +32,6 @@ public enum Movement {
private final int graphY;
private final int graphZ;
- /**
- * Compatibility constructor for physical-coordinate pathfinders.
- */
- public PathNode(double x, double y, double z, Movement movement) {
- this(x, y, z, movement,
- (int) Math.floor(x), (int) Math.floor(y), (int) Math.floor(z));
- }
-
public PathNode(double x, double y, double z, Movement movement,
int graphX, int graphY, int graphZ) {
this.x = x;
diff --git a/src/main/java/ca/atlasengine/pathfinding/result/PathResult.java b/src/main/java/ca/atlasengine/pathfinding/result/PathResult.java
index 455d880..6d896e7 100644
--- a/src/main/java/ca/atlasengine/pathfinding/result/PathResult.java
+++ b/src/main/java/ca/atlasengine/pathfinding/result/PathResult.java
@@ -1,6 +1,8 @@
package ca.atlasengine.pathfinding.result;
-import ca.atlasengine.pathfinding.NavigationPlan;import java.util.List;
+import ca.atlasengine.pathfinding.NavigationPlan;
+
+import java.util.List;
/**
* An immutable search result with basic work metrics for regression testing.
diff --git a/src/main/java/ca/atlasengine/pathfinding/result/PathStop.java b/src/main/java/ca/atlasengine/pathfinding/result/PathStop.java
index 2f82158..480b390 100644
--- a/src/main/java/ca/atlasengine/pathfinding/result/PathStop.java
+++ b/src/main/java/ca/atlasengine/pathfinding/result/PathStop.java
@@ -38,5 +38,12 @@ public enum PathStop {
* No graph search produced this result. Shed, invalid, and synthesised
* results read this, as does any route replayed from a cached plan.
*/
- NOT_SEARCHED
+ NOT_SEARCHED,
+ /**
+ * The open set emptied, but the search read chunks that were not loaded,
+ * which it treats as solid. This proves nothing about reachability: the
+ * same search once those chunks load may find a route. Declared last
+ * because constant order is API.
+ */
+ TERRAIN_UNLOADED
}
diff --git a/src/main/java/ca/atlasengine/pathfinding/search/DiscreteGroundPathfinder.java b/src/main/java/ca/atlasengine/pathfinding/search/DiscreteGroundPathfinder.java
deleted file mode 100644
index 2be4e45..0000000
--- a/src/main/java/ca/atlasengine/pathfinding/search/DiscreteGroundPathfinder.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package ca.atlasengine.pathfinding.search;
-
-import ca.atlasengine.pathfinding.profile.GroundCapabilities;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.internal.search.GroundNodePathfinder;
-import ca.atlasengine.pathfinding.internal.search.GroundSearchSpec;
-import ca.atlasengine.pathfinding.terrain.BlockTraversalData;
-import ca.atlasengine.pathfinding.terrain.MobTraversalProfile;
-import net.minestom.server.collision.BoundingBox;
-import net.minestom.server.coordinate.Point;
-import net.minestom.server.instance.block.Block;
-
-import java.util.List;
-import java.util.Set;
-import static ca.atlasengine.pathfinding.internal.Points.finite;
-
-/**
- * Discrete weighted-A* ground search facade.
- *
- *
This is deliberately separate from adaptive search: adaptive retries are
- * a distinct search strategy.
- */
-public final class DiscreteGroundPathfinder {
- private final GroundNodePathfinder engine = new GroundNodePathfinder();
-
- public PathResult findPath(Block.Getter blocks, Point start, Point target,
- BoundingBox box, MobTraversalProfile profile,
- GroundSearchLimits limits,
- GroundCapabilities capabilities,
- SearchControl control) {
- if (limits == null) throw new IllegalArgumentException("limits");
- if (!finite(start) || !finite(target)) {
- return PathResult.INVALID_REQUEST;
- }
- double maxPathLength = limits.maxPathLength();
- int baseVisited = (int) Math.floor(Math.max(16, maxPathLength) * 16);
- int visited = (int) (baseVisited * limits.maxVisitedMultiplier());
- return engine.findPathToAny(blocks, start, List.of(target), box,
- profile,
- GroundSearchSpec.walking(maxPathLength, limits.reachRange(),
- visited, capabilities),
- control, List.of(), inferredState(blocks, start, profile));
- }
-
- private static EntityTraversalState inferredState(
- Block.Getter blocks, Point start, MobTraversalProfile profile) {
- boolean water = BlockTraversalData.hasWaterFluid(
- blocks.getBlock(start, Block.Getter.Condition.TYPE));
- Set standable = profile.standsOnLava()
- ? Set.of(Block.LAVA) : Set.of();
- return new EntityTraversalState(true, water, standable,
- EntityTraversalState.OVERWORLD_MIN_BUILD_HEIGHT);
- }
-
-}
diff --git a/src/main/java/ca/atlasengine/pathfinding/search/EntityPathfinder.java b/src/main/java/ca/atlasengine/pathfinding/search/EntityPathfinder.java
index 614f7f7..da1acaa 100644
--- a/src/main/java/ca/atlasengine/pathfinding/search/EntityPathfinder.java
+++ b/src/main/java/ca/atlasengine/pathfinding/search/EntityPathfinder.java
@@ -1,7 +1,11 @@
package ca.atlasengine.pathfinding.search;
-import ca.atlasengine.pathfinding.profile.GroundCapabilities;import ca.atlasengine.pathfinding.profile.NavigationMode;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.internal.search.GroundNodePathfinder;
+import ca.atlasengine.pathfinding.internal.LoadedBlocks;
+import ca.atlasengine.pathfinding.internal.search.GroundNodePathfinder;
import ca.atlasengine.pathfinding.internal.search.GroundSearchSpec;
+import ca.atlasengine.pathfinding.profile.GroundCapabilities;
+import ca.atlasengine.pathfinding.profile.NavigationMode;
+import ca.atlasengine.pathfinding.result.PathResult;
import ca.atlasengine.pathfinding.terrain.BlockTraversalData;
import ca.atlasengine.pathfinding.terrain.MobTraversalProfile;
import net.minestom.server.coordinate.Point;
@@ -9,7 +13,8 @@
import net.minestom.server.instance.block.Block;
import java.util.List;
-import static ca.atlasengine.pathfinding.internal.Points.finite;
+
+import static ca.atlasengine.pathfinding.internal.Points.allFinite;
/**
* Dispatches an entity navigation request to the selected movement model.
@@ -28,27 +33,23 @@ public PathResult findPath(NavigationRequest request, SearchControl control) {
public PathResult findPathToAny(
NavigationRequest request, List extends Point> destinations,
SearchControl control) {
- if (destinations == null || destinations.isEmpty()
- || destinations.stream().anyMatch(destination ->
- destination == null || !finite(destination))) {
- return PathResult.INVALID_REQUEST;
- }
- return switch (request.profile().mode()) {
+ if (!allFinite(destinations)) return PathResult.INVALID_REQUEST;
+ NavigationRequest loaded =
+ request.withBlocks(LoadedBlocks.view(request.blocks()));
+ PathResult result = switch (request.profile().mode()) {
case WATER, FLYING ->
- spatial.findPathToAny(request, destinations, control);
+ spatial.findPathToAny(loaded, destinations, control);
case GROUND, WALL_CLIMBER, AMPHIBIOUS ->
- findGround(request, destinations, control);
+ findGround(loaded, destinations, control);
};
+ return LoadedBlocks.qualify(loaded.blocks(), result);
}
-
private PathResult findGround(
NavigationRequest request, List extends Point> destinations,
SearchControl control) {
GroundCapabilities capabilities = request.profile().groundCapabilities();
- int visited = (int) (Math.floor(
- request.nodeSearchRange() * 16)
- * request.maxVisitedMultiplier());
+ int visited = request.maxVisitedNodes();
MobTraversalProfile mobProfile = snapshotRailState(request);
if (request.profile().mode() == NavigationMode.AMPHIBIOUS) {
GroundSearchSpec spec = GroundSearchSpec.amphibious(
@@ -113,11 +114,9 @@ private static Point normalizeGroundTarget(
private static MobTraversalProfile snapshotRailState(NavigationRequest request) {
MobTraversalProfile profile = request.profile().mobProfile();
if (profile.currentlyOnRail()) return profile;
- Block at = request.blocks().getBlock(
- request.start(), Block.Getter.Condition.TYPE);
- Block below = request.blocks().getBlock(
- request.start().add(0, -1, 0), Block.Getter.Condition.TYPE);
- if (!BlockTraversalData.isRail(at) && !BlockTraversalData.isRail(below)) return profile;
+ if (!BlockTraversalData.isRailAt(request.blocks(), request.start())) {
+ return profile;
+ }
return MobTraversalProfile.builder(profile.name() + "_on_rail")
.from(profile)
.currentlyOnRail(true)
diff --git a/src/main/java/ca/atlasengine/pathfinding/search/EntityTraversalState.java b/src/main/java/ca/atlasengine/pathfinding/search/EntityTraversalState.java
index cba74fc..1ed243a 100644
--- a/src/main/java/ca/atlasengine/pathfinding/search/EntityTraversalState.java
+++ b/src/main/java/ca/atlasengine/pathfinding/search/EntityTraversalState.java
@@ -1,12 +1,13 @@
package ca.atlasengine.pathfinding.search;
-import net.minestom.server.instance.block.Block;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
+import net.minestom.server.instance.block.Block;
import java.util.List;
import java.util.Objects;
import java.util.Set;
+
import static ca.atlasengine.pathfinding.internal.Points.finite;
/**
@@ -25,21 +26,14 @@ public record EntityTraversalState(
) {
/** Generous ceiling preventing target-normalization scans from becoming unbounded. */
public static final int MAX_BUILD_HEIGHT_SPAN = 4096;
- static final int OVERWORLD_MIN_BUILD_HEIGHT = -64;
- static final int OVERWORLD_MAX_BUILD_HEIGHT = 319;
+ public static final int OVERWORLD_MIN_BUILD_HEIGHT = -64;
+ public static final int OVERWORLD_MAX_BUILD_HEIGHT = 319;
public static final EntityTraversalState GROUNDED =
new EntityTraversalState(
true, false, Set.of(), OVERWORLD_MIN_BUILD_HEIGHT,
OVERWORLD_MAX_BUILD_HEIGHT, List.of());
- public EntityTraversalState(boolean onGround, boolean inWater,
- Set standableFluids,
- int minBuildHeight) {
- this(onGround, inWater, standableFluids, minBuildHeight,
- OVERWORLD_MAX_BUILD_HEIGHT, List.of());
- }
-
public EntityTraversalState {
if (standableFluids == null
|| standableFluids.stream().anyMatch(Objects::isNull)
@@ -62,7 +56,6 @@ public EntityTraversalState(boolean onGround, boolean inWater,
}
}
-
/** Identity comparison, so a fluid's level never changes the answer. */
public boolean canStandOn(Block block) {
if (!block.liquid()) return false;
diff --git a/src/main/java/ca/atlasengine/pathfinding/search/GroundSearchLimits.java b/src/main/java/ca/atlasengine/pathfinding/search/GroundSearchLimits.java
deleted file mode 100644
index 85c84c8..0000000
--- a/src/main/java/ca/atlasengine/pathfinding/search/GroundSearchLimits.java
+++ /dev/null
@@ -1,118 +0,0 @@
-package ca.atlasengine.pathfinding.search;
-
-/**
- * The three bounds a discrete ground search runs under.
- *
- *
They used to sit adjacent in a parameter list as {@code (64, 0, 1)},
- * where the two doubles could be transposed into a search with a sixteenth of
- * the intended reach. Reach an instance through {@link #ofPathLength(double)}
- * or {@link #builder()}.