From 14feebd8794a27fcc76669ae2420a69f27547230 Mon Sep 17 00:00:00 2001
From: Alexander Parent
Date: Thu, 22 Oct 2026 18:08:35 -0400
Subject: [PATCH] Release 11.0.0: production fixes, API cleanup, simplification
Production fixes:
- Searches read unloaded chunks as solid instead of throwing; a search
walled in by unloaded terrain reports the new PathStop.TERRAIN_UNLOADED
rather than claiming the target unreachable.
- Block-change notifications are filtered by instance and applied on the
controller's own tick, so edits in other instances or threads no longer
replan unrelated routes or fire events off the ticking thread.
- close() keeps a finished navigation's outcome instead of re-reporting it
as cancelled.
API (breaking):
- Controllers are created only through NavigationSystem.controller(Entity)
or the new controllerBuilder(Entity); the public constructors and
builtin() factories, which bypassed admission control, are removed.
- Removed DiscreteGroundPathfinder and GroundSearchLimits; search with
EntityPathfinder and a NavigationRequest.
- Removed notifyBlockChanged(Point), TerrainType.defaultMalus(), the
compatibility constructors on PathNode, NavigationPlan,
EntityTraversalState and AsyncEntityPathfindingService.
- TerrainClassifier is a static utility.
- EntityTargets moves to the root package; spliceCounts() returns the
exported metrics.SpliceCounts. The main compile fails on any exported
signature that names a non-exported type.
Cleanup:
- Shared helpers replace duplicated collision, footprint, route-length,
search-latency, rail, validation and budget code.
- Imports normalised across the tree; tests share TestWorlds and
GroundSearches fixtures; builder chains no longer restate defaults.
Co-Authored-By: Claude Opus 5.5 (1M context)
---
README.md | 8 +-
build.gradle.kts | 8 +-
.../pathfinding/examples/DemoMob.java | 2 +-
.../pathfinding/examples/DemoSession.java | 8 +-
.../pathfinding/examples/Demos.java | 23 +-
.../examples/DocumentedExamples.java | 23 +-
.../pathfinding/examples/NavCommand.java | 5 +-
.../examples/NavigationDemoServer.java | 4 +-
.../AdaptiveNavigationBenchmark.java | 22 +-
.../pathfinding/PathfindingBenchmark.java | 55 ++-
.../pathfinding/PlatformJumpBenchmark.java | 14 +-
.../adaptive/SharedNavigationBenchmark.java | 10 +-
.../AsyncEntityPathfindingService.java | 27 +-
.../pathfinding/ControllerPathScheduler.java | 5 +-
.../ControllerRequestSubmitter.java | 5 +-
.../ControllerSchedulingOptions.java | 8 +-
.../EntityNavigationController.java | 443 ++++++++----------
.../{internal => }/EntityTargets.java | 10 +-
.../pathfinding/NavigationPlan.java | 14 +-
.../pathfinding/NavigationState.java | 13 +-
.../pathfinding/NavigationStrategy.java | 5 +-
.../pathfinding/NavigationSystem.java | 153 +++---
.../pathfinding/PathDebugRenderer.java | 5 +-
.../adaptive/SharedMeshHandle.java | 1 +
.../adaptive/SharedMeshNavigation.java | 4 +-
.../adaptive/SharedMeshOptions.java | 3 +-
.../adaptive/SharedMeshPursuit.java | 62 ++-
.../adaptive/SharedMeshRequest.java | 2 +-
.../pathfinding/adaptive/SharedRouteMesh.java | 2 +-
.../event/NavigationCancelledEvent.java | 5 +-
.../pathfinding/event/NavigationEvent.java | 3 +-
.../influence/BlockAvoidanceInfluence.java | 2 +-
.../pathfinding/influence/NavigationArea.java | 2 +-
.../influence/NavigationZoneInfluence.java | 2 +-
.../influence/ReturnRadiusInfluence.java | 2 +-
.../pathfinding/internal/BoxCollision.java | 50 ++
.../pathfinding/internal/Footprint.java | 35 ++
.../pathfinding/internal/JumpArc.java | 33 +-
.../pathfinding/internal/LoadedBlocks.java | 75 +++
.../pathfinding/internal/Points.java | 11 +
.../pathfinding/internal/RouteLength.java | 33 ++
.../pathfinding/internal/SearchLatency.java | 33 ++
.../adaptive/SharedMeshCoordinator.java | 52 +-
.../internal/adaptive/TargetCell.java | 3 +-
.../movement/ClimbMovementExecutor.java | 18 +-
.../movement/NavigationRequestFactory.java | 51 +-
.../movement/PartialRouteReplanner.java | 26 +-
.../internal/movement/PathPostProcessor.java | 4 +-
.../internal/movement/PathSubmitter.java | 23 -
.../PlatformJumpMovementExecutor.java | 11 +-
.../internal/movement/RouteGeometry.java | 23 +-
.../internal/movement/RouteSplicer.java | 19 +-
.../search/AmphibiousNodeEvaluator.java | 2 +-
.../internal/search/ClearanceProbe.java | 8 +-
.../internal/search/ClimbSupport.java | 27 +-
.../internal/search/FlyNodeEvaluator.java | 24 +-
.../internal/search/GroundNodeEvaluator.java | 30 +-
.../internal/search/GroundNodePathfinder.java | 19 +-
.../internal/search/PlatformJumpSupport.java | 29 +-
.../internal/search/RouteAssembler.java | 5 +-
.../internal/search/SearchNode.java | 1 -
.../internal/search/SpatialNodeEvaluator.java | 19 +-
.../internal/search/SwimNodeEvaluator.java | 18 +-
.../metrics/NavigationMetrics.java | 3 +-
.../pathfinding/metrics/SpliceCounts.java | 20 +
.../profile/BuiltinNavigationProfiles.java | 4 +-
.../profile/GroundCapabilities.java | 19 +-
.../profile/NavigationProfile.java | 45 +-
.../pathfinding/result/PathNode.java | 8 -
.../pathfinding/result/PathResult.java | 4 +-
.../pathfinding/result/PathStop.java | 9 +-
.../search/DiscreteGroundPathfinder.java | 53 ---
.../pathfinding/search/EntityPathfinder.java | 37 +-
.../search/EntityTraversalState.java | 15 +-
.../search/GroundSearchLimits.java | 118 -----
.../pathfinding/search/NavigationRequest.java | 29 +-
.../pathfinding/search/SearchControl.java | 5 +
.../pathfinding/search/SpatialPathfinder.java | 28 +-
.../terrain/BlockTraversalData.java | 21 +-
.../terrain/MobTraversalProfile.java | 6 +
.../terrain/TerrainClassifier.java | 65 ++-
.../pathfinding/terrain/TerrainType.java | 16 +-
.../AdaptiveMeshDifferentialTest.java | 40 +-
.../AdaptiveMeshParityContractTest.java | 45 +-
.../pathfinding/AdaptiveMeshTestSupport.java | 14 +-
.../AdaptiveNavigationCoordinatorTest.java | 73 ++-
.../AdaptivePursuitLivenessTest.java | 5 +-
.../pathfinding/AdvancedTerrainTest.java | 10 +-
.../AdversarialConfigurationTest.java | 19 +-
.../AllowedNavigationAreasTest.java | 8 +-
.../pathfinding/AmphibiousPathfinderTest.java | 14 +-
.../AsyncEntityPathfindingServiceTest.java | 13 +-
.../pathfinding/BlockCellBoundsTest.java | 8 +-
.../pathfinding/BlockManipulationE2ETest.java | 21 +-
.../pathfinding/BlockManipulationTest.java | 14 +-
.../pathfinding/BlockTraversalDataTest.java | 20 +-
.../pathfinding/BuiltinProfileAuditTest.java | 10 +-
.../pathfinding/CachedPlanReplayTest.java | 16 +-
.../ControllerPathSchedulerTest.java | 5 +-
.../ControllerPursuitLivenessTest.java | 18 +-
.../pathfinding/CornerCutPolicyTest.java | 15 +-
.../pathfinding/CropTraversalE2ETest.java | 18 +-
.../pathfinding/DoorRailTraversalTest.java | 9 +-
.../pathfinding/EveryBuiltinMobE2ETest.java | 8 +-
.../FloatingGroundNavigationTest.java | 23 +-
.../GeneratedNavigatorE2ETest.java | 21 +-
.../pathfinding/GroundNodePathfinderTest.java | 190 ++++++--
.../pathfinding/GroundSearchContractTest.java | 63 +--
.../pathfinding/GroundSearches.java | 65 +++
.../pathfinding/GroundShapeClearanceTest.java | 13 +-
.../pathfinding/HardMapOptimalityTest.java | 11 +-
.../pathfinding/HeadClearanceTest.java | 12 +-
.../pathfinding/InfluenceEqualityTest.java | 24 +-
.../pathfinding/LiveInfluenceRoutingTest.java | 17 +-
.../MinestomNavigationE2ETest.java | 88 ++--
.../pathfinding/MobProfileRoutingTest.java | 18 +-
.../pathfinding/ModularControllerE2ETest.java | 25 +-
.../MultiTargetPathfinderTest.java | 10 +-
.../pathfinding/NavigationEventTest.java | 174 +++----
.../pathfinding/NavigationInfluenceTest.java | 14 +-
.../NavigationLifecycleBoundaryE2ETest.java | 24 +-
.../pathfinding/NavigationModifiersTest.java | 19 +-
.../pathfinding/NavigationPlanCacheTest.java | 17 +-
.../pathfinding/NavigationRequestTest.java | 6 +-
.../pathfinding/NavigationSystemTest.java | 206 ++++++--
.../pathfinding/NavigatorEdgeCaseE2ETest.java | 11 +-
.../pathfinding/OpenableClearanceTest.java | 10 +-
.../pathfinding/OpenableFootprintTest.java | 17 +-
.../OpenableSwingClearanceTest.java | 11 +-
.../pathfinding/OpenableSwingCorpusTest.java | 14 +-
.../pathfinding/OpenableSwingE2ETest.java | 10 +-
.../pathfinding/PartialChainRecoveryTest.java | 8 +-
.../pathfinding/PartialRouteCutoffTest.java | 26 +-
.../pathfinding/PathOutputAuditTest.java | 24 +-
.../pathfinding/PathfindingProfileMain.java | 37 +-
.../pathfinding/PlanFollowToleranceTest.java | 17 +-
.../pathfinding/PlatformJumpDoorE2ETest.java | 19 +-
.../pathfinding/PlatformJumpDoorTest.java | 48 +-
.../pathfinding/PlatformJumpE2ETest.java | 50 +-
.../pathfinding/PlatformJumpTest.java | 153 ++++--
.../pathfinding/RouteFreshnessTest.java | 34 +-
.../pathfinding/SearchCancellationTest.java | 19 +-
.../pathfinding/SearchStopReasonTest.java | 14 +-
.../ShapedGeometryAxisSweepTest.java | 19 +-
.../ShapedGeometryFollowE2ETest.java | 16 +-
.../pathfinding/ShapedStepFollowTest.java | 8 +-
.../pathfinding/SharedRouteMeshTest.java | 16 +-
.../pathfinding/StartNodeSamplerTest.java | 3 +-
.../pathfinding/TerminalPartialTest.java | 13 +-
.../TerrainClassificationTest.java | 52 +-
.../pathfinding/TerrainPolicyTest.java | 87 ++--
.../atlasengine/pathfinding/TestWorlds.java | 95 ++++
.../pathfinding/TickOwnershipTest.java | 5 +-
.../pathfinding/WorldBoundaryE2ETest.java | 153 ++++++
.../search/SpatialPathfinderTest.java | 36 +-
.../pathfinding/load/LoadActor.java | 26 +-
.../pathfinding/load/LoadMetrics.java | 2 +-
.../pathfinding/load/NavigationLoadTest.java | 36 +-
.../metrics/NavigationMetricsTest.java | 5 +-
159 files changed, 2875 insertions(+), 1809 deletions(-)
rename src/main/java/ca/atlasengine/pathfinding/{internal => }/EntityTargets.java (89%)
create mode 100644 src/main/java/ca/atlasengine/pathfinding/internal/BoxCollision.java
create mode 100644 src/main/java/ca/atlasengine/pathfinding/internal/Footprint.java
create mode 100644 src/main/java/ca/atlasengine/pathfinding/internal/LoadedBlocks.java
create mode 100644 src/main/java/ca/atlasengine/pathfinding/internal/RouteLength.java
create mode 100644 src/main/java/ca/atlasengine/pathfinding/internal/SearchLatency.java
delete mode 100644 src/main/java/ca/atlasengine/pathfinding/internal/movement/PathSubmitter.java
create mode 100644 src/main/java/ca/atlasengine/pathfinding/metrics/SpliceCounts.java
delete mode 100644 src/main/java/ca/atlasengine/pathfinding/search/DiscreteGroundPathfinder.java
delete mode 100644 src/main/java/ca/atlasengine/pathfinding/search/GroundSearchLimits.java
create mode 100644 src/test/java/ca/atlasengine/pathfinding/GroundSearches.java
create mode 100644 src/test/java/ca/atlasengine/pathfinding/TestWorlds.java
create mode 100644 src/test/java/ca/atlasengine/pathfinding/WorldBoundaryE2ETest.java
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()}.