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 latest = new ConcurrentHashMap<>(); public AsyncEntityPathfindingService(int parallelism, int queueCapacity) { - this(new EntityPathfinder(), parallelism, queueCapacity); + this(parallelism, queueCapacity, new NavigationMetrics()); } /** Records into a recorder shared with other services or coordinators. */ public AsyncEntityPathfindingService(int parallelism, int queueCapacity, NavigationMetrics metrics) { - this(new EntityPathfinder(), parallelism, queueCapacity, metrics); - } - - public AsyncEntityPathfindingService(EntityPathfinder pathfinding, - int parallelism, - int queueCapacity) { - this(pathfinding, parallelism, queueCapacity, new NavigationMetrics()); - } - - public AsyncEntityPathfindingService(EntityPathfinder pathfinding, - int parallelism, - int queueCapacity, - NavigationMetrics metrics) { - if (pathfinding == null) throw new IllegalArgumentException("pathfinding cannot be null"); if (metrics == null) throw new IllegalArgumentException("metrics cannot be null"); if (parallelism <= 0 || queueCapacity <= 0) { throw new IllegalArgumentException("parallelism and queueCapacity must be positive"); } - this.pathfinding = pathfinding; + this.pathfinding = new EntityPathfinder(); this.metrics = metrics; this.queueCapacity = queueCapacity; ThreadPoolExecutor pool = new ThreadPoolExecutor( diff --git a/src/main/java/ca/atlasengine/pathfinding/ControllerPathScheduler.java b/src/main/java/ca/atlasengine/pathfinding/ControllerPathScheduler.java index 22c38d8..ae3be42 100644 --- a/src/main/java/ca/atlasengine/pathfinding/ControllerPathScheduler.java +++ b/src/main/java/ca/atlasengine/pathfinding/ControllerPathScheduler.java @@ -1,6 +1,9 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.search.NavigationRequest;import java.util.ArrayDeque; +import ca.atlasengine.pathfinding.result.PathResult; +import ca.atlasengine.pathfinding.search.NavigationRequest; + +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.HashMap; import java.util.List; diff --git a/src/main/java/ca/atlasengine/pathfinding/ControllerRequestSubmitter.java b/src/main/java/ca/atlasengine/pathfinding/ControllerRequestSubmitter.java index 0e4137f..1e6f2c9 100644 --- a/src/main/java/ca/atlasengine/pathfinding/ControllerRequestSubmitter.java +++ b/src/main/java/ca/atlasengine/pathfinding/ControllerRequestSubmitter.java @@ -1,6 +1,9 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.search.NavigationRequest;import java.util.concurrent.CompletableFuture; +import ca.atlasengine.pathfinding.result.PathResult; +import ca.atlasengine.pathfinding.search.NavigationRequest; + +import java.util.concurrent.CompletableFuture; /** Package-local admission seam used only by entity controllers. */ @FunctionalInterface diff --git a/src/main/java/ca/atlasengine/pathfinding/ControllerSchedulingOptions.java b/src/main/java/ca/atlasengine/pathfinding/ControllerSchedulingOptions.java index 720bba8..29234c4 100644 --- a/src/main/java/ca/atlasengine/pathfinding/ControllerSchedulingOptions.java +++ b/src/main/java/ca/atlasengine/pathfinding/ControllerSchedulingOptions.java @@ -15,15 +15,13 @@ record ControllerSchedulingOptions( int maximumConcurrentSearches, int minimumSearchStallTicks, int shedBackoffTicks) { + static final int DEFAULT_MINIMUM_SEARCH_STALL_TICKS = 60; + static final int DEFAULT_SHED_BACKOFF_TICKS = 20; + ControllerSchedulingOptions { if (maximumDeferredRequests <= 0 || maximumConcurrentSearches <= 0 || minimumSearchStallTicks <= 0 || shedBackoffTicks <= 0) { throw new IllegalArgumentException("invalid controller scheduling options"); } } - - static ControllerSchedulingOptions direct(int parallelism) { - return new ControllerSchedulingOptions( - 1, parallelism, 60, 20); - } } diff --git a/src/main/java/ca/atlasengine/pathfinding/EntityNavigationController.java b/src/main/java/ca/atlasengine/pathfinding/EntityNavigationController.java index 5cfdcea..640a6c1 100644 --- a/src/main/java/ca/atlasengine/pathfinding/EntityNavigationController.java +++ b/src/main/java/ca/atlasengine/pathfinding/EntityNavigationController.java @@ -1,6 +1,6 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.metrics.NavigationMetrics;import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.profile.NavigationMode;import ca.atlasengine.pathfinding.profile.NavigationModifiers;import ca.atlasengine.pathfinding.profile.NavigationProfile;import ca.atlasengine.pathfinding.result.PathNode;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.result.PathStatus;import ca.atlasengine.pathfinding.result.PathStop;import ca.atlasengine.pathfinding.search.NavigationRequest;import ca.atlasengine.pathfinding.event.NavigationCancelledEvent; +import ca.atlasengine.pathfinding.event.NavigationCancelledEvent; import ca.atlasengine.pathfinding.event.NavigationCompletedEvent; import ca.atlasengine.pathfinding.event.NavigationEvent; import ca.atlasengine.pathfinding.event.NavigationEventSink; @@ -11,6 +11,8 @@ import ca.atlasengine.pathfinding.event.RouteReplanEvent; import ca.atlasengine.pathfinding.event.TargetUnreachableEvent; import ca.atlasengine.pathfinding.influence.NavigationInfluence; +import ca.atlasengine.pathfinding.internal.RouteLength; +import ca.atlasengine.pathfinding.internal.SearchLatency; import ca.atlasengine.pathfinding.internal.movement.BlockManipulator; import ca.atlasengine.pathfinding.internal.movement.ClimbMovementExecutor; import ca.atlasengine.pathfinding.internal.movement.MovementContext; @@ -18,13 +20,24 @@ import ca.atlasengine.pathfinding.internal.movement.NavigationRequestFactory; import ca.atlasengine.pathfinding.internal.movement.PartialRouteReplanner; import ca.atlasengine.pathfinding.internal.movement.PathPostProcessor; -import ca.atlasengine.pathfinding.internal.movement.PathSubmitter; import ca.atlasengine.pathfinding.internal.movement.PlatformJumpMovementExecutor; import ca.atlasengine.pathfinding.internal.movement.ProgressMonitor; import ca.atlasengine.pathfinding.internal.movement.RouteGeometry; import ca.atlasengine.pathfinding.internal.movement.RouteSplicer; import ca.atlasengine.pathfinding.internal.movement.VolumeMovementExecutor; import ca.atlasengine.pathfinding.internal.movement.WalkMovementExecutor; +import ca.atlasengine.pathfinding.metrics.NavigationMetrics; +import ca.atlasengine.pathfinding.metrics.SpliceCounts; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.NavigationMode; +import ca.atlasengine.pathfinding.profile.NavigationModifiers; +import ca.atlasengine.pathfinding.profile.NavigationProfile; +import ca.atlasengine.pathfinding.result.PathNode; +import ca.atlasengine.pathfinding.result.PathResult; +import ca.atlasengine.pathfinding.result.PathStatus; +import ca.atlasengine.pathfinding.result.PathStop; +import ca.atlasengine.pathfinding.search.NavigationRequest; +import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Entity; @@ -35,6 +48,7 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; +import java.util.function.Supplier; import java.util.random.RandomGenerator; /** @@ -48,7 +62,9 @@ * controller a caller holds is one it must tick.

*/ 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 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 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 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 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 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()}.

- */ -public final class GroundSearchLimits { - private final double maxPathLength; - private final int reachRange; - private final double maxVisitedMultiplier; - - private GroundSearchLimits(double maxPathLength, int reachRange, - double maxVisitedMultiplier) { - if (!Double.isFinite(maxPathLength) || maxPathLength <= 0 - || reachRange < 0 - || !Double.isFinite(maxVisitedMultiplier) - || maxVisitedMultiplier <= 0) { - throw new IllegalArgumentException("invalid ground search limits"); - } - this.maxPathLength = maxPathLength; - this.reachRange = reachRange; - this.maxVisitedMultiplier = maxVisitedMultiplier; - } - - /** - * The common case: a geometric path-length bound, an exact destination, - * and the unscaled expansion budget. - */ - public static GroundSearchLimits ofPathLength(double maxPathLength) { - return new GroundSearchLimits(maxPathLength, 0, 1); - } - - public static Builder builder() { - return new Builder(); - } - - /** How far an expanded node may sit from the requested start. */ - public double maxPathLength() { - return maxPathLength; - } - - /** How close to the goal counts as reaching it. */ - public int reachRange() { - return reachRange; - } - - /** Scales the expansion budget derived from {@link #maxPathLength()}. */ - public double maxVisitedMultiplier() { - return maxVisitedMultiplier; - } - - public GroundSearchLimits withReachRange(int value) { - return new GroundSearchLimits( - maxPathLength, value, maxVisitedMultiplier); - } - - public GroundSearchLimits withMaxVisitedMultiplier(double value) { - return new GroundSearchLimits(maxPathLength, reachRange, value); - } - - @Override - public boolean equals(Object object) { - if (this == object) return true; - if (!(object instanceof GroundSearchLimits other)) return false; - return Double.compare(maxPathLength, other.maxPathLength) == 0 - && reachRange == other.reachRange - && Double.compare(maxVisitedMultiplier, - other.maxVisitedMultiplier) == 0; - } - - @Override - public int hashCode() { - int result = Double.hashCode(maxPathLength); - result = 31 * result + Integer.hashCode(reachRange); - return 31 * result + Double.hashCode(maxVisitedMultiplier); - } - - @Override - public String toString() { - return "GroundSearchLimits[maxPathLength=" + maxPathLength - + ", reachRange=" + reachRange - + ", maxVisitedMultiplier=" + maxVisitedMultiplier + "]"; - } - - /** Reach range defaults to zero and the visit multiplier to one. */ - public static final class Builder { - private double maxPathLength = 64; - private int reachRange; - private double maxVisitedMultiplier = 1; - - private Builder() { - } - - public Builder maxPathLength(double value) { - maxPathLength = value; - return this; - } - - public Builder reachRange(int value) { - reachRange = value; - return this; - } - - public Builder maxVisitedMultiplier(double value) { - maxVisitedMultiplier = value; - return this; - } - - public GroundSearchLimits build() { - return new GroundSearchLimits( - maxPathLength, reachRange, maxVisitedMultiplier); - } - } -} diff --git a/src/main/java/ca/atlasengine/pathfinding/search/NavigationRequest.java b/src/main/java/ca/atlasengine/pathfinding/search/NavigationRequest.java index eeb5b4d..32d1bef 100644 --- a/src/main/java/ca/atlasengine/pathfinding/search/NavigationRequest.java +++ b/src/main/java/ca/atlasengine/pathfinding/search/NavigationRequest.java @@ -1,13 +1,17 @@ package ca.atlasengine.pathfinding.search; -import ca.atlasengine.pathfinding.profile.NavigationModifiers;import ca.atlasengine.pathfinding.profile.NavigationProfile;import ca.atlasengine.pathfinding.influence.NavigationInfluence; +import ca.atlasengine.pathfinding.influence.NavigationInfluence; +import ca.atlasengine.pathfinding.internal.Footprint; import ca.atlasengine.pathfinding.internal.search.GroundSearchSpec; +import ca.atlasengine.pathfinding.profile.NavigationModifiers; +import ca.atlasengine.pathfinding.profile.NavigationProfile; 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.Objects; + import static ca.atlasengine.pathfinding.internal.Points.finite; /** @@ -50,7 +54,6 @@ public record NavigationRequest( influences = List.copyOf(influences); } - private static boolean valid(BoundingBox box) { return finite(box.relativeStart()) && finite(box.relativeEnd()) && box.width() > 0 && box.height() > 0 && box.depth() > 0 @@ -61,10 +64,8 @@ private static boolean valid(BoundingBox box) { } private static long footprintCells(BoundingBox box) { - long width = Math.max(1, (long) Math.floor(box.width() + 1)); - long height = Math.max(1, (long) Math.floor(box.height() + 1)); - long depth = Math.max(1, (long) Math.floor(box.depth() + 1)); - return width * height * depth; + return (long) Footprint.width(box) * Footprint.height(box) + * Footprint.depth(box); } /** @@ -79,6 +80,22 @@ public static Builder builder( return new Builder(blocks, start, target, boundingBox, profile); } + /** + * Node expansions this request may spend: sixteen per block of node search + * range, scaled by the visit multiplier. + */ + int maxVisitedNodes() { + return (int) (Math.floor(nodeSearchRange * 16) * maxVisitedMultiplier); + } + + /** The same request reading the world through another getter. */ + NavigationRequest withBlocks(Block.Getter view) { + return view == blocks ? this : new NavigationRequest( + view, start, target, boundingBox, profile, maxPathLength, + reachRange, maxVisitedMultiplier, influences, seaLevel, + entityState, nodeSearchRange); + } + public NavigationRequest withModifiers(NavigationModifiers modifiers) { if (modifiers == null) throw new IllegalArgumentException("modifiers"); return new NavigationRequest( diff --git a/src/main/java/ca/atlasengine/pathfinding/search/SearchControl.java b/src/main/java/ca/atlasengine/pathfinding/search/SearchControl.java index e4450e7..56953f6 100644 --- a/src/main/java/ca/atlasengine/pathfinding/search/SearchControl.java +++ b/src/main/java/ca/atlasengine/pathfinding/search/SearchControl.java @@ -31,6 +31,11 @@ public boolean timedOut() { return System.nanoTime() >= deadlineNanos; } + /** Whether the search must stop now, by cancellation or deadline. */ + public boolean interrupted() { + return cancelled() || timedOut(); + } + /** Whether inner evaluator loops need periodic cooperative polling. */ public boolean interruptible() { return cancellation != CancellationToken.NONE diff --git a/src/main/java/ca/atlasengine/pathfinding/search/SpatialPathfinder.java b/src/main/java/ca/atlasengine/pathfinding/search/SpatialPathfinder.java index 032bf3b..c6ca626 100644 --- a/src/main/java/ca/atlasengine/pathfinding/search/SpatialPathfinder.java +++ b/src/main/java/ca/atlasengine/pathfinding/search/SpatialPathfinder.java @@ -1,13 +1,17 @@ package ca.atlasengine.pathfinding.search; -import ca.atlasengine.pathfinding.profile.NavigationMode;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.internal.search.FlyNodeEvaluator; +import ca.atlasengine.pathfinding.internal.LoadedBlocks; +import ca.atlasengine.pathfinding.internal.search.FlyNodeEvaluator; import ca.atlasengine.pathfinding.internal.search.PathSearch; import ca.atlasengine.pathfinding.internal.search.SpatialNodeEvaluator; import ca.atlasengine.pathfinding.internal.search.SwimNodeEvaluator; +import ca.atlasengine.pathfinding.profile.NavigationMode; +import ca.atlasengine.pathfinding.result.PathResult; import net.minestom.server.coordinate.Point; import java.util.List; -import static ca.atlasengine.pathfinding.internal.Points.finite; + +import static ca.atlasengine.pathfinding.internal.Points.allFinite; /** * Weighted A* volume search for flying and swimming movement. @@ -22,20 +26,16 @@ public PathResult findPathToAny( SearchControl control) { NavigationMode type = request.profile().mode(); if ((type != NavigationMode.FLYING && type != NavigationMode.WATER) - || destinations == null || destinations.isEmpty() - || destinations.stream().anyMatch(destination -> - destination == null || !finite(destination))) { + || !allFinite(destinations)) { return PathResult.INVALID_REQUEST; } - int maxVisited = (int) (Math.floor( - request.nodeSearchRange() * 16) - * request.maxVisitedMultiplier()); + NavigationRequest loaded = + request.withBlocks(LoadedBlocks.view(request.blocks())); SpatialNodeEvaluator evaluator = type == NavigationMode.WATER - ? new SwimNodeEvaluator(request, control) - : new FlyNodeEvaluator(request, control); - return PathSearch.find(evaluator, destinations, - request.maxPathLength(), request.reachRange(), maxVisited, - control); + ? new SwimNodeEvaluator(loaded, control) + : new FlyNodeEvaluator(loaded, control); + return LoadedBlocks.qualify(loaded.blocks(), PathSearch.find( + evaluator, destinations, request.maxPathLength(), + request.reachRange(), request.maxVisitedNodes(), control)); } - } diff --git a/src/main/java/ca/atlasengine/pathfinding/terrain/BlockTraversalData.java b/src/main/java/ca/atlasengine/pathfinding/terrain/BlockTraversalData.java index 900257f..9a2fa57 100644 --- a/src/main/java/ca/atlasengine/pathfinding/terrain/BlockTraversalData.java +++ b/src/main/java/ca/atlasengine/pathfinding/terrain/BlockTraversalData.java @@ -5,8 +5,9 @@ import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.BlockFace; -import java.util.Set; +import java.util.Arrays; import java.util.BitSet; +import java.util.Set; /** * Block-family predicates used by the navigation terrain classifiers. @@ -24,7 +25,8 @@ public final class BlockTraversalData { "decorated_pot", "sculk_sensor", "stonecutter", "soul_sand", "azalea", "flowering_azalea", "farmland", "heavy_core", "chorus_plant", "chest", "trapped_chest", "ender_chest", - "brewing_stand", "conduit", "lectern", "iron_chain", "copper_chain", "exposed_copper_chain", + "brewing_stand", "conduit", "lectern", "iron_chain", "copper_chain", + "exposed_copper_chain", "weathered_copper_chain", "oxidized_copper_chain", "waxed_copper_chain", "waxed_exposed_copper_chain", "waxed_weathered_copper_chain", "waxed_oxidized_copper_chain", "composter", @@ -67,7 +69,7 @@ public final class BlockTraversalData { private static final BitSet SLABS = tag("slabs"); private static final BitSet LAND_NEVER_BLOCKS = ids(LAND_NEVER_IDS); private static final BitSet[] LAND_NEVER_MEMBERS = - java.util.Arrays.stream(LAND_NEVER_TAGS) + Arrays.stream(LAND_NEVER_TAGS) .map(BlockTraversalData::tag).toArray(BitSet[]::new); private BlockTraversalData() { @@ -166,6 +168,13 @@ public static boolean isRail(Block block) { return contains(RAILS, block); } + /** Whether an entity standing at {@code position} is on a rail. */ + public static boolean isRailAt(Block.Getter blocks, Point position) { + return isRail(blocks.getBlock(position, Block.Getter.Condition.TYPE)) + || isRail(blocks.getBlock(position.add(0, -1, 0), + Block.Getter.Condition.TYPE)); + } + public static boolean isLeaves(Block block) { return contains(LEAVES, block); } @@ -195,11 +204,7 @@ public static boolean isClimbableAt( Block.Getter blocks, int x, int y, int z) { Block block = blocks.getBlock(x, y, z, Block.Getter.Condition.TYPE); if (isClimbable(block)) return true; - if (!isTrapdoor(block) - || !Boolean.parseBoolean( - property(block, "open", "false"))) { - return false; - } + if (!isTrapdoor(block) || !isOpen(block)) return false; Block below = blocks.getBlock( x, y - 1, z, Block.Getter.Condition.TYPE); return below.compare(Block.LADDER) diff --git a/src/main/java/ca/atlasengine/pathfinding/terrain/MobTraversalProfile.java b/src/main/java/ca/atlasengine/pathfinding/terrain/MobTraversalProfile.java index 90d9b24..512126e 100644 --- a/src/main/java/ca/atlasengine/pathfinding/terrain/MobTraversalProfile.java +++ b/src/main/java/ca/atlasengine/pathfinding/terrain/MobTraversalProfile.java @@ -8,6 +8,7 @@ import java.util.EnumMap; import java.util.Map; import java.util.Objects; +import java.util.Set; /** * Immutable movement capabilities and per-terrain cost overrides. @@ -338,6 +339,11 @@ public boolean standsOnLava() { return standsOnLava; } + /** The fluids this profile treats as ground, for a traversal snapshot. */ + public Set standableFluids() { + return standsOnLava ? Set.of(Block.LAVA) : Set.of(); + } + /** * Blocks this profile names itself, asked before the baseline classifier. */ diff --git a/src/main/java/ca/atlasengine/pathfinding/terrain/TerrainClassifier.java b/src/main/java/ca/atlasengine/pathfinding/terrain/TerrainClassifier.java index f118a5c..f60bd90 100644 --- a/src/main/java/ca/atlasengine/pathfinding/terrain/TerrainClassifier.java +++ b/src/main/java/ca/atlasengine/pathfinding/terrain/TerrainClassifier.java @@ -1,5 +1,6 @@ package ca.atlasengine.pathfinding.terrain; +import ca.atlasengine.pathfinding.internal.Footprint; import ca.atlasengine.pathfinding.search.SearchControl; import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Point; @@ -10,16 +11,19 @@ import java.util.Set; /** - * Stateless Minestom block-to-navigation-terrain classifier. Shared entry - * points are static so movement evaluators can reuse them without retaining - * classifier state. + * Stateless Minestom block-to-navigation-terrain classifier. */ public final class TerrainClassifier { private static final int[][] AXIAL_DIRECTIONS = { {1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, {0, -1, 0}, {0, 0, 1}, {0, 0, -1} }; - public TerrainType classify(Block.Getter blocks, Point point, + + private TerrainClassifier() { + } + + /** Classifies the cell under an entity centred at {@code point}. */ + public static TerrainType classify(Block.Getter blocks, Point point, BoundingBox box, MobTraversalProfile profile) { int minX = (int) Math.floor(point.x() - box.width() / 2); int minY = point.blockY(); @@ -31,15 +35,16 @@ public TerrainType classify(Block.Getter blocks, Point point, * Classifies an integer minimum-corner graph node. The graph coordinate is * not the entity's physical center. */ - public TerrainType classifyAnchored(Block.Getter blocks, - int minX, int minY, int minZ, - BoundingBox box, - MobTraversalProfile profile) { + public static TerrainType classifyAnchored(Block.Getter blocks, + int minX, int minY, int minZ, + BoundingBox box, + MobTraversalProfile profile) { return classifyAnchored(blocks, minX, minY, minZ, box, profile, false, SearchControl.NONE); } - public TerrainType classifyAnchored( + /** As above, abandoning a cell scan the search has been told to stop. */ + public static TerrainType classifyAnchored( Block.Getter blocks, int minX, int minY, int minZ, BoundingBox box, MobTraversalProfile profile, SearchControl control) { @@ -47,14 +52,11 @@ public TerrainType classifyAnchored( blocks, minX, minY, minZ, box, profile, false, control); } - public TerrainType classifyAnchoredAmphibious( - Block.Getter blocks, int minX, int minY, int minZ, - BoundingBox box, MobTraversalProfile profile) { - return classifyAnchored(blocks, minX, minY, minZ, box, profile, - true, SearchControl.NONE); - } - - public TerrainType classifyAnchoredAmphibious( + /** + * The amphibious variant, which marks water against a barrier as + * {@link TerrainType#WATER_BORDER} at the cell's own level. + */ + public static TerrainType classifyAnchoredAmphibious( Block.Getter blocks, int minX, int minY, int minZ, BoundingBox box, MobTraversalProfile profile, SearchControl control) { @@ -62,15 +64,15 @@ public TerrainType classifyAnchoredAmphibious( blocks, minX, minY, minZ, box, profile, true, control); } - private TerrainType classifyAnchored( + private static TerrainType classifyAnchored( Block.Getter blocks, int minX, int minY, int minZ, BoundingBox box, MobTraversalProfile profile, boolean waterBorderAtCellLevel, SearchControl control) { TerrainClassification classification = profile.classification(); Set types = EnumSet.noneOf(TerrainType.class); - 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); // Where the entity stands once it occupies this node, which is what // an openable's swung panel has to leave room for. Only a profile // that swings one ever needs to know. @@ -88,7 +90,7 @@ private TerrainType classifyAnchored( 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( @@ -175,7 +177,7 @@ private static boolean crossable(Block.Getter blocks, int x, int y, int z, x - stepX, y, z - stepZ, Block.Getter.Condition.TYPE)); } - private TerrainType cellType( + private static TerrainType cellType( Block.Getter blocks, Block block, int x, int y, int z, MobTraversalProfile profile, TerrainClassification classification, boolean waterBorderAtCellLevel, Point rest, BoundingBox box) { @@ -219,7 +221,7 @@ public static TerrainType raw(Block block) { if (block.compare(Block.LAVA)) return TerrainType.LAVA; if (BlockTraversalData.isBurning(block)) return TerrainType.FIRE; if (BlockTraversalData.isDoor(block)) { - if (Boolean.parseBoolean(property(block, "open", "false"))) { + if (BlockTraversalData.isOpen(block)) { return TerrainType.DOOR_OPEN; } return BlockTraversalData.isHandOpenableDoor(block) @@ -230,7 +232,7 @@ public static TerrainType raw(Block block) { if (BlockTraversalData.isLeaves(block)) return TerrainType.LEAVES; if (BlockTraversalData.isFence(block) || (BlockTraversalData.isFenceGate(block) - && !Boolean.parseBoolean(property(block, "open", "false")))) { + && !BlockTraversalData.isOpen(block))) { return TerrainType.FENCE; } if (block.compare(Block.WATER) || block.liquid()) { @@ -267,13 +269,7 @@ public static TerrainType transform(TerrainType type, MobTraversalProfile profil return type; } - private static String property( - Block block, String name, String defaultValue) { - String value = block.getProperty(name); - return value == null ? defaultValue : value; - } - - private TerrainType pathTypeAt(Block.Getter blocks, Block block, + private static TerrainType pathTypeAt(Block.Getter blocks, Block block, int x, int y, int z, MobTraversalProfile profile, TerrainClassification classification, @@ -304,7 +300,7 @@ private TerrainType pathTypeAt(Block.Getter blocks, Block block, return standingOn(support, blocks, x, y, z, classification); } - private TerrainType standingOn(TerrainType support, + private static TerrainType standingOn(TerrainType support, Block.Getter blocks, int x, int y, int z, TerrainClassification classification) { return switch (support) { @@ -370,7 +366,7 @@ public static TerrainType select(Set types, highest = malus; } } - if ((int) Math.floor(box.width() + 1) > 1) { + if (Footprint.width(box) > 1) { boolean currentIsCheaper = profile.malus(current) < highest; if (currentIsCheaper && profile.malus(TerrainType.BIG_MOBS_CLOSE_TO_DANGER) < highest) { @@ -382,5 +378,4 @@ public static TerrainType select(Set types, } return selected; } - } diff --git a/src/main/java/ca/atlasengine/pathfinding/terrain/TerrainType.java b/src/main/java/ca/atlasengine/pathfinding/terrain/TerrainType.java index 0142948..e6cad75 100644 --- a/src/main/java/ca/atlasengine/pathfinding/terrain/TerrainType.java +++ b/src/main/java/ca/atlasengine/pathfinding/terrain/TerrainType.java @@ -79,19 +79,5 @@ public enum TerrainType { * its anchor cell does not. Caps the penalty so a large mob is discouraged * from grazing danger without being walled off by its own width. */ - BIG_MOBS_CLOSE_TO_DANGER; - - /** - * The baseline cost of entering a cell of this category. - * - * @deprecated a category no longer owns its cost. Call - * {@link TerrainCosts#baseline(TerrainType)} for the same value, or - * {@link MobTraversalProfile#malus(TerrainType)} for the cost a - * specific mob actually pays, which is what nearly every caller wants. - * Retained so existing binaries and sources keep working. - */ - @Deprecated(since = "10.3.0") - public double defaultMalus() { - return TerrainCosts.baseline(this); - } + BIG_MOBS_CLOSE_TO_DANGER } diff --git a/src/test/java/ca/atlasengine/pathfinding/AdaptiveMeshDifferentialTest.java b/src/test/java/ca/atlasengine/pathfinding/AdaptiveMeshDifferentialTest.java index f8b3b2c..a43a9e6 100644 --- a/src/test/java/ca/atlasengine/pathfinding/AdaptiveMeshDifferentialTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/AdaptiveMeshDifferentialTest.java @@ -1,12 +1,10 @@ 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.NavigationProfile;import ca.atlasengine.pathfinding.profile.PlatformJumpCapabilities;import ca.atlasengine.pathfinding.result.PathNode;import ca.atlasengine.pathfinding.search.EntityPathfinder;import ca.atlasengine.pathfinding.search.EntityTraversalState;import ca.atlasengine.pathfinding.search.NavigationRequest;import ca.atlasengine.pathfinding.search.SearchControl;import ca.atlasengine.pathfinding.adaptive.SharedMeshRetention; -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.SharedMeshRetention; import ca.atlasengine.pathfinding.adaptive.SharedMeshSource; -import ca.atlasengine.pathfinding.NavigationPlan; -import ca.atlasengine.pathfinding.internal.adaptive.NavigationPlanParity; import ca.atlasengine.pathfinding.influence.AllowedNavigationAreas; import ca.atlasengine.pathfinding.influence.BlockAvoidanceInfluence; import ca.atlasengine.pathfinding.influence.EntityFearInfluence; @@ -16,6 +14,19 @@ import ca.atlasengine.pathfinding.influence.NavigationInfluence; import ca.atlasengine.pathfinding.influence.NavigationZoneInfluence; import ca.atlasengine.pathfinding.influence.ReturnRadiusInfluence; +import ca.atlasengine.pathfinding.internal.adaptive.NavigationPlanParity; +import ca.atlasengine.pathfinding.internal.adaptive.SharedMeshCoordinator; +import ca.atlasengine.pathfinding.metrics.NavigationMetrics; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.ClimbableCapabilities; +import ca.atlasengine.pathfinding.profile.GroundCapabilities; +import ca.atlasengine.pathfinding.profile.NavigationProfile; +import ca.atlasengine.pathfinding.profile.PlatformJumpCapabilities; +import ca.atlasengine.pathfinding.result.PathNode; +import ca.atlasengine.pathfinding.search.EntityPathfinder; +import ca.atlasengine.pathfinding.search.EntityTraversalState; +import ca.atlasengine.pathfinding.search.NavigationRequest; +import ca.atlasengine.pathfinding.search.SearchControl; import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; @@ -259,7 +270,7 @@ private static void runToggle( .promotionRequests(2 + random.nextInt(3)) .retentionRequests(1) .maximumRegions(maximumRegions) - .build()); + .build(), new NavigationMetrics()); List chosen = pick( new ArrayList<>(scenario.candidates), ACTORS, random); // One footprint for the whole population, so the traversal snapshot is @@ -326,7 +337,7 @@ private static void runPursuit( // with every position can overflow it. .maximumTargetsPerRegion( PURSUIT_TARGET_CELLS * 2) - .build()); + .build(), new NavigationMetrics()); List cells = pick(new ArrayList<>(scenario.candidates), PURSUIT_TARGET_CELLS, random); List stations = pick(new ArrayList<>(scenario.candidates), @@ -384,7 +395,7 @@ void keyedLambdaInfluencesShareOneRegionAndStillMatchIndividualSearch() { SearchControl.NONE))), SharedMeshPolicy.builder() .promotionRequests(2).retentionRequests(1) - .build()); + .build(), new NavigationMetrics()); List handles = new ArrayList<>(); long tick = 0; try { @@ -467,7 +478,7 @@ void equalSizedFootprintsWithDifferentAnchorsNeverShareCertifiedPlans() { SearchControl.NONE))), SharedMeshPolicy.builder() .promotionRequests(2).retentionRequests(1) - .build()); + .build(), new NavigationMetrics()); Pos start = new Pos(0.5, 1, 0.5); Pos target = new Pos(10.5, 1, 0.5); NavigationRequest first = request(world, start, target, @@ -526,7 +537,7 @@ private static void runScenario(long seed, Totals totals) { return CompletableFuture.completedFuture( NavigationPlan.from(submitted, oracle.findPath( submitted, SearchControl.NONE))); - }, config); + }, config, new NavigationMetrics()); List handles = new ArrayList<>(); long tick = 0; try { @@ -898,13 +909,18 @@ NavigationProfile profile() { ClimbableCapabilities.STANDARD)); case JUMP -> withGround(base, base.groundCapabilities().withPlatformJump( - PlatformJumpCapabilities.builder().maxHorizontalDistance(3).maxRise(1).maxDrop(1).apexClearance(1).build())); + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(3) + .maxRise(1) + .maxDrop(1) + .apexClearance(1) + .build())); }; } private static NavigationProfile withGround( NavigationProfile base, GroundCapabilities capabilities) { - return NavigationProfile.builder(base.mode(), base.mobProfile(), capabilities).allowBreaching(base.allowBreaching()).prefersShallowWater(base.prefersShallowWater()).avoidSun(base.avoidSun()).directPathIgnoresFluids(base.directPathIgnoresFluids()).pathToTargetsBelowSurface(base.pathToTargetsBelowSurface()).build(); + return base.withGroundCapabilities(capabilities); } } diff --git a/src/test/java/ca/atlasengine/pathfinding/AdaptiveMeshParityContractTest.java b/src/test/java/ca/atlasengine/pathfinding/AdaptiveMeshParityContractTest.java index cd7f8ef..100d556 100644 --- a/src/test/java/ca/atlasengine/pathfinding/AdaptiveMeshParityContractTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/AdaptiveMeshParityContractTest.java @@ -1,12 +1,24 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.profile.ClimbableCapabilities;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.result.PathStatus;import ca.atlasengine.pathfinding.search.EntityPathfinder;import ca.atlasengine.pathfinding.search.EntityTraversalState;import ca.atlasengine.pathfinding.search.NavigationRequest;import ca.atlasengine.pathfinding.search.SearchControl;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.NavigationPlanParity; import ca.atlasengine.pathfinding.influence.NavigationZoneInfluence; +import ca.atlasengine.pathfinding.internal.adaptive.NavigationPlanParity; +import ca.atlasengine.pathfinding.internal.adaptive.SharedMeshCoordinator; +import ca.atlasengine.pathfinding.metrics.NavigationMetrics; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.ClimbableCapabilities; +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.result.PathStatus; +import ca.atlasengine.pathfinding.search.EntityPathfinder; +import ca.atlasengine.pathfinding.search.EntityTraversalState; +import ca.atlasengine.pathfinding.search.NavigationRequest; +import ca.atlasengine.pathfinding.search.SearchControl; import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; @@ -16,9 +28,9 @@ import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicInteger; -import java.util.Set; import static org.junit.jupiter.api.Assertions.*; @@ -202,7 +214,7 @@ private static void assertCertifiedReplay(Fixture fixture) { assertTrue(NavigationPlanParity.requestCompatible( request, fixture.plan), fixture.name); return CompletableFuture.completedFuture(fixture.plan); - }, config); + }, config, new NavigationMetrics()); NavigationRequest request = NavigationRequest.builder( new TestWorld(), fixture.plan.start(), fixture.plan.target(), BOX, fixture.plan.profile()) @@ -241,7 +253,9 @@ private static NavigationPlan assertRealReplay( pathfinder.findPath( submitted, SearchControl.NONE))); }, SharedMeshPolicy.builder() - .promotionRequests(2).retentionRequests(1).build()); + .promotionRequests(2) + .retentionRequests(1) + .build(), new NavigationMetrics()); NavigationPlan first = coordinator.request( "seed-1", "target", 9, 0, request).plan().join(); NavigationPlan second = coordinator.request( @@ -379,7 +393,7 @@ private static Fixture fixture( nodes.getLast().y(), 0.5); return new Fixture(name, new NavigationPlan( start, target, BOX, profile, PathStatus.FOUND, - nodes, nodes.size(), nodes.size() * 8)); + nodes, nodes.size(), nodes.size() * 8, List.of())); } private static NavigationProfile profile(EntityType type) { @@ -388,14 +402,19 @@ private static NavigationProfile profile(EntityType type) { private static NavigationProfile climbingProfile() { NavigationProfile base = profile(EntityType.ZOMBIE); - return 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(); + return base.withGroundCapabilities(base.groundCapabilities() + .withClimbables(ClimbableCapabilities.STANDARD)); } private static NavigationProfile jumpingProfile() { NavigationProfile base = profile(EntityType.ZOMBIE); - return NavigationProfile.builder(base.mode(), base.mobProfile(), base.groundCapabilities().withPlatformJump( - PlatformJumpCapabilities.builder().maxHorizontalDistance(3).maxRise(1).maxDrop(1).apexClearance(1).build())).allowBreaching(base.allowBreaching()).prefersShallowWater(base.prefersShallowWater()).avoidSun(base.avoidSun()).directPathIgnoresFluids(base.directPathIgnoresFluids()).pathToTargetsBelowSurface(base.pathToTargetsBelowSurface()).build(); + return base.withGroundCapabilities(base.groundCapabilities() + .withPlatformJump(PlatformJumpCapabilities.builder() + .maxHorizontalDistance(3) + .maxRise(1) + .maxDrop(1) + .apexClearance(1) + .build())); } private static NavigationPlan copy( @@ -403,7 +422,7 @@ private static NavigationPlan copy( return new NavigationPlan( source.start(), source.target(), source.boundingBox(), source.profile(), source.status(), nodes, - source.visitedNodes(), source.examinedNeighbors()); + source.visitedNodes(), source.examinedNeighbors(), List.of()); } private record Fixture(String name, NavigationPlan plan) { diff --git a/src/test/java/ca/atlasengine/pathfinding/AdaptiveMeshTestSupport.java b/src/test/java/ca/atlasengine/pathfinding/AdaptiveMeshTestSupport.java index 9e6804e..2d4e4d7 100644 --- a/src/test/java/ca/atlasengine/pathfinding/AdaptiveMeshTestSupport.java +++ b/src/test/java/ca/atlasengine/pathfinding/AdaptiveMeshTestSupport.java @@ -1,13 +1,15 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.search.NavigationRequest;import ca.atlasengine.pathfinding.adaptive.SharedMeshPolicy; -import ca.atlasengine.pathfinding.internal.adaptive.SharedMeshCoordinator; -import ca.atlasengine.pathfinding.adaptive.SharedMeshHandle; -import ca.atlasengine.pathfinding.adaptive.SharedMeshSource; import ca.atlasengine.pathfinding.NavigationPlan; -import ca.atlasengine.pathfinding.internal.adaptive.NavigationPlanParity; +import ca.atlasengine.pathfinding.adaptive.SharedMeshHandle; import ca.atlasengine.pathfinding.adaptive.SharedMeshNavigation; +import ca.atlasengine.pathfinding.adaptive.SharedMeshPolicy; import ca.atlasengine.pathfinding.adaptive.SharedMeshRequest; +import ca.atlasengine.pathfinding.adaptive.SharedMeshSource; +import ca.atlasengine.pathfinding.internal.adaptive.NavigationPlanParity; +import ca.atlasengine.pathfinding.internal.adaptive.SharedMeshCoordinator; +import ca.atlasengine.pathfinding.metrics.NavigationMetrics; +import ca.atlasengine.pathfinding.search.NavigationRequest; import java.util.concurrent.atomic.AtomicInteger; @@ -30,7 +32,7 @@ static NavigationPlan promoteAndReplay( }, SharedMeshPolicy.builder() .promotionRequests(2) .retentionRequests(1) - .build())) { + .build(), new NavigationMetrics())) { NavigationPlan first = coordinator.request( "seed-1", "target", 1, 0, request).plan().join(); NavigationPlan second = coordinator.request( diff --git a/src/test/java/ca/atlasengine/pathfinding/AdaptiveNavigationCoordinatorTest.java b/src/test/java/ca/atlasengine/pathfinding/AdaptiveNavigationCoordinatorTest.java index 39ef344..27deb82 100644 --- a/src/test/java/ca/atlasengine/pathfinding/AdaptiveNavigationCoordinatorTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/AdaptiveNavigationCoordinatorTest.java @@ -1,12 +1,10 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.metrics.NavigationMetrics;import ca.atlasengine.pathfinding.metrics.NavigationMetricsSnapshot;import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.profile.ClimbableCapabilities;import ca.atlasengine.pathfinding.profile.NavigationProfile;import ca.atlasengine.pathfinding.result.PathNode;import ca.atlasengine.pathfinding.result.PathStatus;import ca.atlasengine.pathfinding.search.EntityPathfinder;import ca.atlasengine.pathfinding.search.EntityTraversalState;import ca.atlasengine.pathfinding.search.NavigationRequest;import ca.atlasengine.pathfinding.search.SearchControl;import ca.atlasengine.pathfinding.adaptive.SharedMeshRetention; -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.SharedMeshRetention; import ca.atlasengine.pathfinding.adaptive.SharedMeshSource; -import ca.atlasengine.pathfinding.NavigationPlan; -import ca.atlasengine.pathfinding.internal.adaptive.NavigationPlanParity; import ca.atlasengine.pathfinding.influence.AllowedNavigationAreas; import ca.atlasengine.pathfinding.influence.BlockAvoidanceInfluence; import ca.atlasengine.pathfinding.influence.EntityFearInfluence; @@ -16,6 +14,19 @@ import ca.atlasengine.pathfinding.influence.NavigationInfluence; import ca.atlasengine.pathfinding.influence.NavigationZoneInfluence; import ca.atlasengine.pathfinding.influence.ReturnRadiusInfluence; +import ca.atlasengine.pathfinding.internal.adaptive.NavigationPlanParity; +import ca.atlasengine.pathfinding.internal.adaptive.SharedMeshCoordinator; +import ca.atlasengine.pathfinding.metrics.NavigationMetrics; +import ca.atlasengine.pathfinding.metrics.NavigationMetricsSnapshot; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.ClimbableCapabilities; +import ca.atlasengine.pathfinding.profile.NavigationProfile; +import ca.atlasengine.pathfinding.result.PathNode; +import ca.atlasengine.pathfinding.result.PathStatus; +import ca.atlasengine.pathfinding.search.EntityPathfinder; +import ca.atlasengine.pathfinding.search.EntityTraversalState; +import ca.atlasengine.pathfinding.search.NavigationRequest; +import ca.atlasengine.pathfinding.search.SearchControl; import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; @@ -28,6 +39,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.TreeSet; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -38,7 +50,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -241,7 +252,9 @@ void togglingLiveTraversalStateSharesARegionWithoutSharingACertificate() { NavigationPlan.from(submitted, oracle.findPath( submitted, SearchControl.NONE))); }, SharedMeshPolicy.builder() - .promotionRequests(2).retentionRequests(1).build()); + .promotionRequests(2) + .retentionRequests(1) + .build(), new NavigationMetrics()); // Five blocks over the floor, so a grounded snapshot rounds to the // empty cell the mob occupies while an airborne one walks down to the // surface. Both are legal starts and they are not the same node. @@ -426,7 +439,9 @@ void subBlockTargetMovementReusesOneFieldAndStillMatchesIndividualSearch() { NavigationPlan.from(submitted, oracle.findPath( submitted, SearchControl.NONE))); }, SharedMeshPolicy.builder() - .promotionRequests(2).retentionRequests(1).build()); + .promotionRequests(2) + .retentionRequests(1) + .build(), new NavigationMetrics()); Pos start = new Pos(1.5, 1, 5.5); List jittered = List.of( new Pos(12.37, 1, 4.81), new Pos(12.62, 1, 4.12), @@ -518,7 +533,7 @@ void aFullRegionReclaimsColdDestinationsInsteadOfDroppingPursuers() { SharedMeshCoordinator coordinator = new SharedMeshCoordinator( (actor, request) -> completedPlan(request, searches), - config); + config, new NavigationMetrics()); TestWorld world = new TestWorld(); Pos start = new Pos(1, 1, 1); try { @@ -548,7 +563,7 @@ void intermediateCorridorSourceRequiresOneCertificateBeforeReuse() { new SharedMeshCoordinator((actor, request) -> { searches.incrementAndGet(); return CompletableFuture.completedFuture(corridorPlan(request)); - }, config); + }, config, new NavigationMetrics()); TestWorld world = new TestWorld(); Pos target = new Pos(8.5, 1, 0.5); NavigationRequest full = request(world, @@ -590,7 +605,9 @@ void cheaperMeshSuffixThatDiffersFromAStarCertificateFallsBack() { ? detourPlan(request) : corridorPlan(request)); }, SharedMeshPolicy.builder() - .promotionRequests(2).retentionRequests(1).build()); + .promotionRequests(2) + .retentionRequests(1) + .build(), new NavigationMetrics()); TestWorld world = new TestWorld(); Pos target = new Pos(4.5, 1, 0.5); NavigationRequest full = request(world, @@ -625,7 +642,9 @@ void revisionsCleanupTargetsRegionsAndSupersededActorsDeterministically() { .maximumRegions(4).build(); SharedMeshCoordinator coordinator = new SharedMeshCoordinator( - (actor, request) -> completedPlan(request, searches), config); + (actor, + request) -> completedPlan(request, + searches), config, new NavigationMetrics()); TestWorld world = new TestWorld(); NavigationRequest request = request(world, new Pos(1.5, 1, 1.5), new Pos(10.5, 1, 1.5), SMALL, GROUND); @@ -689,7 +708,9 @@ void capacityUsesUntrackedAStarFallbackAndUnloadDropsOnlyMatchingCell() { .build(); SharedMeshCoordinator coordinator = new SharedMeshCoordinator( - (actor, request) -> completedPlan(request, searches), config); + (actor, + request) -> completedPlan(request, + searches), config, new NavigationMetrics()); TestWorld world = new TestWorld(); coordinator.request("inside", "target", 1, 0, request(world, new Pos(1, 1, 1), new Pos(8, 1, 1), @@ -725,7 +746,7 @@ void concurrentCompletionsPromoteAtomicallyAndLateResultsCannotResurrectCleanup( new CompletableFuture<>(); pending.add(result); return result; - }, config); + }, config, new NavigationMetrics()); TestWorld world = new TestWorld(); NavigationRequest request = request(world, new Pos(1.5, 1, 1.5), new Pos(30.5, 1, 1.5), SMALL, GROUND); @@ -773,7 +794,9 @@ void movingTargetHistoryIsHardBounded() { .maximumTargetsPerRegion(2).build(); SharedMeshCoordinator coordinator = new SharedMeshCoordinator( - (actor, request) -> completedPlan(request, searches), config); + (actor, + request) -> completedPlan(request, + searches), config, new NavigationMetrics()); TestWorld world = new TestWorld(); coordinator.request("one", "player", 1, 0, @@ -801,7 +824,7 @@ void removingActorCancelsUnfinishedSearchAndPreventsLateRetention() { new SharedMeshCoordinator((actor, request) -> pending, SharedMeshPolicy.builder() .promotionRequests(2).retentionRequests(1) - .build()); + .build(), new NavigationMetrics()); TestWorld world = new TestWorld(); NavigationRequest request = request(world, new Pos(1, 1, 1), new Pos(20, 1, 1), SMALL, GROUND); @@ -997,7 +1020,7 @@ void everyRetainedPlanMapIsReportedSeparatelyAndDrainsWithItsRegion() { SharedMeshCoordinator coordinator = new SharedMeshCoordinator( (actor, request) -> completedPlan(request, searches), - config); + config, new NavigationMetrics()); TestWorld world = new TestWorld(); List handles = new ArrayList<>(); @@ -1166,7 +1189,9 @@ void racingCertificationsNeverPublishAPlanThatDivergesFromIndividualSearch() pending.put(actor, future); return future; }, SharedMeshPolicy.builder() - .promotionRequests(4).retentionRequests(1).build()); + .promotionRequests(4) + .retentionRequests(1) + .build(), new NavigationMetrics()); List starts = List.of(new Pos(0.5, 1, 0.5), new Pos(0.5, 1, 15.5), new Pos(15.5, 1, 0.5), new Pos(1.5, 1, 8.5)); Pos destination = new Pos(14.5, 1, 14.5); @@ -1231,7 +1256,7 @@ private static SharedMeshCoordinator pendingCoordinator( pending.put(actor, future); return future; }, SharedMeshPolicy.builder() - .promotionRequests(2).retentionRequests(1).build()); + .promotionRequests(2).retentionRequests(1).build(), new NavigationMetrics()); } /** @@ -1315,7 +1340,9 @@ private static SharedMeshCoordinator coordinator( SharedMeshPolicy config = SharedMeshPolicy.builder() .promotionRequests(threshold).retentionRequests(1).build(); return new SharedMeshCoordinator( - (actor, request) -> completedPlan(request, searches), config); + (actor, + request) -> completedPlan(request, + searches), config, new NavigationMetrics()); } private static CompletableFuture completedPlan( @@ -1332,7 +1359,7 @@ private static CompletableFuture completedPlan( new PathNode(target.x(), target.y(), target.z(), PathNode.Movement.WALK, target.blockX(), target.blockY(), target.blockZ())), - 2, 8)); + 2, 8, List.of())); } private static NavigationPlan corridorPlan(NavigationRequest request) { @@ -1347,7 +1374,7 @@ private static NavigationPlan corridorPlan(NavigationRequest request) { return new NavigationPlan( request.start(), request.target(), request.boundingBox(), request.profile(), PathStatus.FOUND, nodes, - nodes.size(), nodes.size() * 8); + nodes.size(), nodes.size() * 8, List.of()); } private static NavigationPlan detourPlan(NavigationRequest request) { @@ -1358,7 +1385,7 @@ private static NavigationPlan detourPlan(NavigationRequest request) { new PathNode(4.5, 1, 0.5, PathNode.Movement.WALK, 4, 1, 0)); return new NavigationPlan( request.start(), request.target(), request.boundingBox(), - request.profile(), PathStatus.FOUND, nodes, 4, 32); + request.profile(), PathStatus.FOUND, nodes, 4, 32, List.of()); } private static NavigationRequest request( diff --git a/src/test/java/ca/atlasengine/pathfinding/AdaptivePursuitLivenessTest.java b/src/test/java/ca/atlasengine/pathfinding/AdaptivePursuitLivenessTest.java index e7568a2..fc0e9c0 100644 --- a/src/test/java/ca/atlasengine/pathfinding/AdaptivePursuitLivenessTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/AdaptivePursuitLivenessTest.java @@ -1,6 +1,7 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.metrics.NavigationMetricsSnapshot;import ca.atlasengine.pathfinding.adaptive.SharedMeshPursuit; +import ca.atlasengine.pathfinding.adaptive.SharedMeshPursuit; +import ca.atlasengine.pathfinding.metrics.NavigationMetricsSnapshot; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.Entity; @@ -19,7 +20,6 @@ import static ca.atlasengine.pathfinding.PursuitHarness.SURFACE_Y; import static ca.atlasengine.pathfinding.PursuitHarness.loadedGatedFlatInstance; import static ca.atlasengine.pathfinding.PursuitHarness.orbit; - import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -159,5 +159,4 @@ private static Pursuit pursue(Env env, int latencyTicks) { env.destroyInstance(instance); } } - } diff --git a/src/test/java/ca/atlasengine/pathfinding/AdvancedTerrainTest.java b/src/test/java/ca/atlasengine/pathfinding/AdvancedTerrainTest.java index 486409a..c34bfd7 100644 --- a/src/test/java/ca/atlasengine/pathfinding/AdvancedTerrainTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/AdvancedTerrainTest.java @@ -1,6 +1,10 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.GroundCapabilities;import ca.atlasengine.pathfinding.result.PathNode;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.search.DiscreteGroundPathfinder;import ca.atlasengine.pathfinding.search.GroundSearchLimits;import ca.atlasengine.pathfinding.search.SearchControl;import ca.atlasengine.pathfinding.terrain.MobTraversalProfile; +import ca.atlasengine.pathfinding.profile.GroundCapabilities; +import ca.atlasengine.pathfinding.result.PathNode; +import ca.atlasengine.pathfinding.result.PathResult; +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; @@ -10,7 +14,6 @@ import static org.junit.jupiter.api.Assertions.*; class AdvancedTerrainTest { - private final DiscreteGroundPathfinder engine = new DiscreteGroundPathfinder(); @ParameterizedTest @ValueSource(doubles = {0.4, 0.8, 1.4}) @@ -51,7 +54,8 @@ private static double verticalChange(PathResult result) { private PathResult find(TestWorld world, Pos start, Pos goal, BoundingBox box, double maxLength) { - return engine.findPath(world, start, goal, box, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(maxLength).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.STANDARD, SearchControl.NONE); + return GroundSearches.find(world, start, goal, box, MobTraversalProfile.DEFAULT, maxLength, + 8, GroundCapabilities.STANDARD, SearchControl.NONE); } private static TestWorld flat(int radius) { diff --git a/src/test/java/ca/atlasengine/pathfinding/AdversarialConfigurationTest.java b/src/test/java/ca/atlasengine/pathfinding/AdversarialConfigurationTest.java index bd702c3..85a9a43 100644 --- a/src/test/java/ca/atlasengine/pathfinding/AdversarialConfigurationTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/AdversarialConfigurationTest.java @@ -1,8 +1,16 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.profile.GroundCapabilities;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.result.PathStatus;import ca.atlasengine.pathfinding.search.EntityPathfinder;import ca.atlasengine.pathfinding.search.EntityTraversalState;import ca.atlasengine.pathfinding.search.NavigationRequest;import ca.atlasengine.pathfinding.search.SearchControl;import ca.atlasengine.pathfinding.influence.NavigationZoneInfluence; import ca.atlasengine.pathfinding.influence.NavigationArea; +import ca.atlasengine.pathfinding.influence.NavigationZoneInfluence; import ca.atlasengine.pathfinding.influence.ReturnRadiusInfluence; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.GroundCapabilities; +import ca.atlasengine.pathfinding.result.PathResult; +import ca.atlasengine.pathfinding.result.PathStatus; +import ca.atlasengine.pathfinding.search.EntityPathfinder; +import ca.atlasengine.pathfinding.search.EntityTraversalState; +import ca.atlasengine.pathfinding.search.NavigationRequest; +import ca.atlasengine.pathfinding.search.SearchControl; import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.EntityType; @@ -18,10 +26,13 @@ class AdversarialConfigurationTest { @Test void verticalCapabilitiesHaveFiniteWorkBounds() { assertThrows(IllegalArgumentException.class, - () -> GroundCapabilities.builder().maxStepHeight(17).maxFallDistance(5).allowDiagonal(true).build()); + () -> GroundCapabilities.builder().maxStepHeight(17).build()); assertThrows(IllegalArgumentException.class, - () -> GroundCapabilities.builder().maxStepHeight(1).maxFallDistance(1025).allowDiagonal(true).build()); - GroundCapabilities.builder().maxStepHeight(GroundCapabilities.MAX_STEP_HEIGHT).maxFallDistance(GroundCapabilities.MAX_FALL_DISTANCE).allowDiagonal(true).build(); + () -> GroundCapabilities.builder().maxFallDistance(1025).build()); + GroundCapabilities.builder() + .maxStepHeight(GroundCapabilities.MAX_STEP_HEIGHT) + .maxFallDistance(GroundCapabilities.MAX_FALL_DISTANCE) + .build(); } @Test diff --git a/src/test/java/ca/atlasengine/pathfinding/AllowedNavigationAreasTest.java b/src/test/java/ca/atlasengine/pathfinding/AllowedNavigationAreasTest.java index 3e49d9d..ab58055 100644 --- a/src/test/java/ca/atlasengine/pathfinding/AllowedNavigationAreasTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/AllowedNavigationAreasTest.java @@ -1,8 +1,14 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.profile.NavigationProfile;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.influence.AllowedNavigationAreas; +import ca.atlasengine.pathfinding.influence.AllowedNavigationAreas; import ca.atlasengine.pathfinding.influence.InfluenceResult; import ca.atlasengine.pathfinding.influence.NavigationArea; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.NavigationProfile; +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 net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; diff --git a/src/test/java/ca/atlasengine/pathfinding/AmphibiousPathfinderTest.java b/src/test/java/ca/atlasengine/pathfinding/AmphibiousPathfinderTest.java index a89a71e..50433dc 100644 --- a/src/test/java/ca/atlasengine/pathfinding/AmphibiousPathfinderTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/AmphibiousPathfinderTest.java @@ -1,7 +1,16 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.profile.NavigationMode;import ca.atlasengine.pathfinding.result.PathNode;import ca.atlasengine.pathfinding.result.PathNodeCost;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.search.EntityPathfinder;import ca.atlasengine.pathfinding.search.EntityTraversalState;import ca.atlasengine.pathfinding.search.NavigationRequest;import ca.atlasengine.pathfinding.search.SearchControl;import ca.atlasengine.pathfinding.influence.NavigationInfluence; +import ca.atlasengine.pathfinding.influence.NavigationInfluence; import ca.atlasengine.pathfinding.influence.NavigationZoneInfluence; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.NavigationMode; +import ca.atlasengine.pathfinding.result.PathNode; +import ca.atlasengine.pathfinding.result.PathNodeCost; +import ca.atlasengine.pathfinding.result.PathResult; +import ca.atlasengine.pathfinding.search.EntityPathfinder; +import ca.atlasengine.pathfinding.search.EntityTraversalState; +import ca.atlasengine.pathfinding.search.NavigationRequest; +import ca.atlasengine.pathfinding.search.SearchControl; import ca.atlasengine.pathfinding.terrain.MobTraversalProfile; import ca.atlasengine.pathfinding.terrain.TerrainType; import net.minestom.server.collision.BoundingBox; @@ -11,6 +20,7 @@ import org.junit.jupiter.api.Test; import java.util.List; +import java.util.Set; import static org.junit.jupiter.api.Assertions.*; @@ -207,7 +217,7 @@ private PathResult find(TestWorld world, Vec start, Vec target, .entityState(new EntityTraversalState( true, world.getBlock(start).key().value().equals("water"), - java.util.Set.of(), -64)) + Set.of(), -64, EntityTraversalState.OVERWORLD_MAX_BUILD_HEIGHT, List.of())) .build(), SearchControl.NONE); } diff --git a/src/test/java/ca/atlasengine/pathfinding/AsyncEntityPathfindingServiceTest.java b/src/test/java/ca/atlasengine/pathfinding/AsyncEntityPathfindingServiceTest.java index b010820..79ca393 100644 --- a/src/test/java/ca/atlasengine/pathfinding/AsyncEntityPathfindingServiceTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/AsyncEntityPathfindingServiceTest.java @@ -1,6 +1,14 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.metrics.NavigationMetrics;import ca.atlasengine.pathfinding.metrics.NavigationMetricsSnapshot;import ca.atlasengine.pathfinding.profile.GroundCapabilities;import ca.atlasengine.pathfinding.profile.NavigationMode;import ca.atlasengine.pathfinding.profile.NavigationProfile;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.result.PathStatus;import ca.atlasengine.pathfinding.search.NavigationRequest;import ca.atlasengine.pathfinding.terrain.MobTraversalProfile; +import ca.atlasengine.pathfinding.metrics.NavigationMetrics; +import ca.atlasengine.pathfinding.metrics.NavigationMetricsSnapshot; +import ca.atlasengine.pathfinding.profile.GroundCapabilities; +import ca.atlasengine.pathfinding.profile.NavigationMode; +import ca.atlasengine.pathfinding.profile.NavigationProfile; +import ca.atlasengine.pathfinding.result.PathResult; +import ca.atlasengine.pathfinding.result.PathStatus; +import ca.atlasengine.pathfinding.search.NavigationRequest; +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; @@ -16,7 +24,8 @@ class AsyncEntityPathfindingServiceTest { private static final BoundingBox BOX = new BoundingBox(0.8, 1.8, 0.8); - private static final NavigationProfile GROUND = NavigationProfile.builder(NavigationMode.GROUND, MobTraversalProfile.DEFAULT, GroundCapabilities.STANDARD).allowBreaching(false).prefersShallowWater(false).avoidSun(false).build(); + private static final NavigationProfile GROUND = NavigationProfile.builder(NavigationMode.GROUND, + MobTraversalProfile.DEFAULT, GroundCapabilities.STANDARD).build(); @Test void runsAllRequestsOnBoundedNamedWorkers() { diff --git a/src/test/java/ca/atlasengine/pathfinding/BlockCellBoundsTest.java b/src/test/java/ca/atlasengine/pathfinding/BlockCellBoundsTest.java index 995f968..d31fe2f 100644 --- a/src/test/java/ca/atlasengine/pathfinding/BlockCellBoundsTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/BlockCellBoundsTest.java @@ -1,9 +1,15 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.profile.NavigationProfile;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.influence.AllowedNavigationAreas; +import ca.atlasengine.pathfinding.influence.AllowedNavigationAreas; import ca.atlasengine.pathfinding.influence.NavigationArea; import ca.atlasengine.pathfinding.influence.NavigationInfluence; import ca.atlasengine.pathfinding.influence.NavigationZoneInfluence; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.NavigationProfile; +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 net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; diff --git a/src/test/java/ca/atlasengine/pathfinding/BlockManipulationE2ETest.java b/src/test/java/ca/atlasengine/pathfinding/BlockManipulationE2ETest.java index c4609f6..0335248 100644 --- a/src/test/java/ca/atlasengine/pathfinding/BlockManipulationE2ETest.java +++ b/src/test/java/ca/atlasengine/pathfinding/BlockManipulationE2ETest.java @@ -1,11 +1,14 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.profile.ClimbableCapabilities;import ca.atlasengine.pathfinding.profile.NavigationModifiers;import ca.atlasengine.pathfinding.profile.NavigationProfile;import ca.atlasengine.pathfinding.internal.movement.BlockManipulator; +import ca.atlasengine.pathfinding.internal.movement.BlockManipulator; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.ClimbableCapabilities; +import ca.atlasengine.pathfinding.profile.NavigationModifiers; +import ca.atlasengine.pathfinding.profile.NavigationProfile; import ca.atlasengine.pathfinding.terrain.BlockManipulationCapabilities; import ca.atlasengine.pathfinding.terrain.MobTraversalProfile; import ca.atlasengine.pathfinding.terrain.OpenableBlockFamily; import net.minestom.server.collision.BoundingBox; -import net.minestom.server.coordinate.ChunkRange; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.EntityCreature; @@ -50,7 +53,7 @@ void requestScopedCapabilityOpensAndCrossesAFenceGate(Env env) { new AsyncEntityPathfindingService(1, 8)) { ControlledCreature zombie = spawn(instance, new Pos(0.5, SURFACE, 0.5)); EntityNavigationController controller = - EntityNavigationController.builtin(zombie, service, 0.18); + TestWorlds.builtinController(zombie, service, 0.18); zombie.controller = controller; controller.moveTo(new Pos(5.5, SURFACE, 0.5), NavigationModifiers.builder() @@ -80,7 +83,7 @@ void ordinaryProfileNeitherPlansThroughNorOpensTheSameGate(Env env) { new AsyncEntityPathfindingService(1, 8)) { ControlledCreature zombie = spawn(instance, new Pos(0.5, SURFACE, 0.5)); EntityNavigationController controller = - EntityNavigationController.builtin(zombie, service, 0.18); + TestWorlds.builtinController(zombie, service, 0.18); zombie.controller = controller; controller.moveTo(new Pos(5.5, SURFACE, 0.5)); @@ -103,7 +106,7 @@ void closeBehindRestoresTheDoorOnceTheMobHasPassedIt(Env env) { new AsyncEntityPathfindingService(1, 8)) { ControlledCreature zombie = spawn(instance, new Pos(0.5, SURFACE, 0.5)); EntityNavigationController controller = - new EntityNavigationController(zombie, service, + TestWorlds.controller(zombie, service, zombieProfile(BlockManipulationCapabilities.STANDARD .closingBehind()), 0.18); zombie.controller = controller; @@ -130,7 +133,7 @@ void closeBehindIsSkippedBeyondTheConfiguredRange(Env env) { new AsyncEntityPathfindingService(1, 8)) { ControlledCreature zombie = spawn(instance, new Pos(0.5, SURFACE, 0.5)); EntityNavigationController controller = - new EntityNavigationController(zombie, service, + TestWorlds.controller(zombie, service, zombieProfile(BlockManipulationCapabilities.STANDARD .closingBehind(0.5)), 0.18); zombie.controller = controller; @@ -170,7 +173,7 @@ void climbsPastTheHatchItOpensBesideTheLadder(Env env) { new AsyncEntityPathfindingService(1, 8)) { ControlledCreature zombie = spawn(instance, new Pos(0.5, SURFACE, 0.5)); EntityNavigationController controller = - new EntityNavigationController(zombie, service, + TestWorlds.controller(zombie, service, climbing(zombieProfile( BlockManipulationCapabilities.of( OpenableBlockFamily.TRAPDOOR))), @@ -290,9 +293,7 @@ private static Instance corridor(Env env) { } private static Instance preparedFlat(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 3, - (x, z) -> instance.loadChunk(x, z).join()); + Instance instance = TestWorlds.loadedFlat(env, 3); return instance; } diff --git a/src/test/java/ca/atlasengine/pathfinding/BlockManipulationTest.java b/src/test/java/ca/atlasengine/pathfinding/BlockManipulationTest.java index 4d8df67..35490b9 100644 --- a/src/test/java/ca/atlasengine/pathfinding/BlockManipulationTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/BlockManipulationTest.java @@ -1,6 +1,12 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.profile.GroundCapabilities;import ca.atlasengine.pathfinding.profile.NavigationModifiers;import ca.atlasengine.pathfinding.profile.NavigationProfile;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.search.DiscreteGroundPathfinder;import ca.atlasengine.pathfinding.search.GroundSearchLimits;import ca.atlasengine.pathfinding.search.SearchControl;import ca.atlasengine.pathfinding.terrain.BlockManipulationCapabilities; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.GroundCapabilities; +import ca.atlasengine.pathfinding.profile.NavigationModifiers; +import ca.atlasengine.pathfinding.profile.NavigationProfile; +import ca.atlasengine.pathfinding.result.PathResult; +import ca.atlasengine.pathfinding.search.SearchControl; +import ca.atlasengine.pathfinding.terrain.BlockManipulationCapabilities; import ca.atlasengine.pathfinding.terrain.BlockTraversalData; import ca.atlasengine.pathfinding.terrain.MobTraversalProfile; import ca.atlasengine.pathfinding.terrain.OpenableBlockFamily; @@ -36,7 +42,6 @@ class BlockManipulationTest { .withProperty("hinge", "left") .withProperty("half", "lower") .withProperty("open", "false"); - private final TerrainClassifier classifier = new TerrainClassifier(); @Test void capabilityConstantsMatchTheBuiltinDoorContract() { @@ -234,12 +239,13 @@ void openableFamilyMatchesTheHandOpenableBlockSetTypes() { private static PathResult cross(TestWorld world, MobTraversalProfile profile) { - return new DiscreteGroundPathfinder().findPath(world, new Vec(-0.5, 1, 0.5), new Vec(3.5, 1, 0.5), CELL, profile, GroundSearchLimits.builder().maxPathLength(16).reachRange(0).maxVisitedMultiplier(4).build(), GroundCapabilities.STANDARD, SearchControl.NONE); + return GroundSearches.find(world, new Vec(-0.5, 1, 0.5), new Vec(3.5, 1, 0.5), CELL, + profile, 16, 4, GroundCapabilities.STANDARD, SearchControl.NONE); } private TerrainType classify(TestWorld world, int x, MobTraversalProfile profile) { - return classifier.classifyAnchored(world, x, 1, 0, CELL, profile); + return TerrainClassifier.classifyAnchored(world, x, 1, 0, CELL, profile); } private static Block block(Block.Getter blocks, int x) { diff --git a/src/test/java/ca/atlasengine/pathfinding/BlockTraversalDataTest.java b/src/test/java/ca/atlasengine/pathfinding/BlockTraversalDataTest.java index 278ec03..8020f9e 100644 --- a/src/test/java/ca/atlasengine/pathfinding/BlockTraversalDataTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/BlockTraversalDataTest.java @@ -8,7 +8,6 @@ import org.junit.jupiter.api.Test; import java.util.List; -import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -16,7 +15,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; class BlockTraversalDataTest { - private final TerrainClassifier classifier = new TerrainClassifier(); @Test void registryTagsResolveDirectAndNestedMembers() { @@ -78,9 +76,9 @@ void copperDoorsUseHandOpenableDoorSemantics() { assertTrue(BlockTraversalData.isHandOpenableDoor(Block.OAK_DOOR)); assertFalse(BlockTraversalData.isHandOpenableDoor(Block.IRON_DOOR)); assertEquals(TerrainType.DOOR_WOOD_CLOSED, - classifier.raw(Block.COPPER_DOOR.withProperty("open", "false"))); + TerrainClassifier.raw(Block.COPPER_DOOR.withProperty("open", "false"))); assertEquals(TerrainType.DOOR_IRON_CLOSED, - classifier.raw(Block.IRON_DOOR.withProperty("open", "false"))); + TerrainClassifier.raw(Block.IRON_DOOR.withProperty("open", "false"))); } @Test @@ -125,13 +123,13 @@ void burningClassificationHonorsCampfireLitState() { @Test void classifierTreatsPartialCollisionOverridesAsExpected() { - assertEquals(TerrainType.OPEN, classifier.raw(Block.WHITE_CARPET)); - assertEquals(TerrainType.OPEN, classifier.raw(Block.CANDLE)); - assertEquals(TerrainType.BLOCKED, classifier.raw(Block.END_ROD)); - assertEquals(TerrainType.BLOCKED, classifier.raw(Block.OAK_SLAB)); - assertEquals(TerrainType.BLOCKED, classifier.raw( + assertEquals(TerrainType.OPEN, TerrainClassifier.raw(Block.WHITE_CARPET)); + assertEquals(TerrainType.OPEN, TerrainClassifier.raw(Block.CANDLE)); + assertEquals(TerrainType.BLOCKED, TerrainClassifier.raw(Block.END_ROD)); + assertEquals(TerrainType.BLOCKED, TerrainClassifier.raw(Block.OAK_SLAB)); + assertEquals(TerrainType.BLOCKED, TerrainClassifier.raw( Block.OAK_SLAB.withProperty("waterlogged", "true"))); - assertEquals(TerrainType.BLOCKED, classifier.raw(Block.CAKE)); - assertEquals(TerrainType.FENCE, classifier.raw(Block.CINNABAR_WALL)); + assertEquals(TerrainType.BLOCKED, TerrainClassifier.raw(Block.CAKE)); + assertEquals(TerrainType.FENCE, TerrainClassifier.raw(Block.CINNABAR_WALL)); } } diff --git a/src/test/java/ca/atlasengine/pathfinding/BuiltinProfileAuditTest.java b/src/test/java/ca/atlasengine/pathfinding/BuiltinProfileAuditTest.java index 04bebe0..2a7daf9 100644 --- a/src/test/java/ca/atlasengine/pathfinding/BuiltinProfileAuditTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/BuiltinProfileAuditTest.java @@ -1,10 +1,13 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.profile.NavigationMode;import ca.atlasengine.pathfinding.profile.NavigationProfile;import ca.atlasengine.pathfinding.terrain.BuiltinMobProfiles; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.NavigationMode; +import ca.atlasengine.pathfinding.profile.NavigationProfile; +import ca.atlasengine.pathfinding.terrain.BuiltinMobProfiles; import ca.atlasengine.pathfinding.terrain.MobTraversalProfile; import ca.atlasengine.pathfinding.terrain.TerrainType; -import net.minestom.server.entity.EntityType; import net.minestom.server.entity.EntityCreature; +import net.minestom.server.entity.EntityType; import net.minestom.server.entity.metadata.AgeableMobMeta; import org.junit.jupiter.api.Test; @@ -101,7 +104,8 @@ void allSpecializedEntityMappingsMatchExpectedBehaviorFamilies() { assertProfile(MobTraversalProfile.ANIMAL, EntityType.HOGLIN); assertProfile(MobTraversalProfile.CAMEL, EntityType.CAMEL, EntityType.CAMEL_HUSK); assertProfile(MobTraversalProfile.PIGLIN, EntityType.PIGLIN, EntityType.PIGLIN_BRUTE); - assertProfile(MobTraversalProfile.NAUTILUS, EntityType.NAUTILUS, EntityType.ZOMBIE_NAUTILUS); + assertProfile(MobTraversalProfile.NAUTILUS, EntityType.NAUTILUS, + EntityType.ZOMBIE_NAUTILUS); assertProfile(MobTraversalProfile.FLOATING_DEFAULT, EntityType.SLIME, EntityType.MAGMA_CUBE, EntityType.SULFUR_CUBE, EntityType.SPIDER, EntityType.CAVE_SPIDER, EntityType.CREEPER); diff --git a/src/test/java/ca/atlasengine/pathfinding/CachedPlanReplayTest.java b/src/test/java/ca/atlasengine/pathfinding/CachedPlanReplayTest.java index 3be6a7b..8c0a537 100644 --- a/src/test/java/ca/atlasengine/pathfinding/CachedPlanReplayTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/CachedPlanReplayTest.java @@ -1,8 +1,14 @@ 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.EntityPathfinder;import ca.atlasengine.pathfinding.search.NavigationRequest;import ca.atlasengine.pathfinding.search.SearchControl;import ca.atlasengine.pathfinding.NavigationPlan; +import ca.atlasengine.pathfinding.NavigationPlan; import ca.atlasengine.pathfinding.NavigationPlanCache; -import net.minestom.server.coordinate.ChunkRange; +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.EntityPathfinder; +import ca.atlasengine.pathfinding.search.NavigationRequest; +import ca.atlasengine.pathfinding.search.SearchControl; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.EntityCreature; @@ -40,7 +46,7 @@ void controllerWalksCachedPlanWithoutSubmittingAnotherSearch(Env env) { NavigationProfile profile = BuiltinNavigationProfiles.forEntity(zombie); EntityNavigationController controller = - new EntityNavigationController( + TestWorlds.controller( zombie, service, profile, 0.18); zombie.controller = controller; NavigationPlan plan = planned(instance, zombie, profile); @@ -140,9 +146,7 @@ private static ControlledCreature spawn( } private static Instance prepared(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 3, - (x, z) -> instance.loadChunk(x, z).join()); + Instance instance = TestWorlds.loadedFlat(env, 3); return instance; } diff --git a/src/test/java/ca/atlasengine/pathfinding/ControllerPathSchedulerTest.java b/src/test/java/ca/atlasengine/pathfinding/ControllerPathSchedulerTest.java index 98f309a..14bed07 100644 --- a/src/test/java/ca/atlasengine/pathfinding/ControllerPathSchedulerTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/ControllerPathSchedulerTest.java @@ -1,6 +1,9 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.search.NavigationRequest;import net.minestom.server.collision.BoundingBox; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.result.PathResult; +import ca.atlasengine.pathfinding.search.NavigationRequest; +import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.EntityType; import net.minestom.server.instance.block.Block; diff --git a/src/test/java/ca/atlasengine/pathfinding/ControllerPursuitLivenessTest.java b/src/test/java/ca/atlasengine/pathfinding/ControllerPursuitLivenessTest.java index 9ec98be..10f47f6 100644 --- a/src/test/java/ca/atlasengine/pathfinding/ControllerPursuitLivenessTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/ControllerPursuitLivenessTest.java @@ -1,6 +1,8 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.metrics.NavigationMetricsSnapshot;import ca.atlasengine.pathfinding.result.PathNode;import net.minestom.server.coordinate.Point; +import ca.atlasengine.pathfinding.metrics.NavigationMetricsSnapshot; +import ca.atlasengine.pathfinding.result.PathNode; +import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.EntityCreature; import net.minestom.server.entity.EntityType; @@ -17,7 +19,6 @@ import static ca.atlasengine.pathfinding.PursuitHarness.SURFACE_Y; import static ca.atlasengine.pathfinding.PursuitHarness.loadedGatedFlatInstance; import static ca.atlasengine.pathfinding.PursuitHarness.orbit; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -66,8 +67,8 @@ void intervalMoveToKeepsRoutesPastTheRecomputeInterval(Env env) { void aLandedSearchIsReadBeforeAQueuedReplanSupersedesIt(Env env) { PursuitHarness.SearchGate gate = new PursuitHarness.SearchGate(); Instance instance = loadedGatedFlatInstance(env, gate); - EntityCreature actor = new EntityCreature(EntityType.ZOMBIE); - actor.setInstance(instance, new Pos(0.5, SURFACE_Y, 0.5)).join(); + EntityCreature actor = TestWorlds.spawn(instance, EntityType.ZOMBIE, + new Pos(0.5, SURFACE_Y, 0.5)); for (int settle = 0; settle < 3; settle++) env.tick(); NavigationSystem system = NavigationSystem.builder() @@ -112,8 +113,8 @@ void aLandedSearchIsReadBeforeAQueuedReplanSupersedesIt(Env env) { void aSearchThatNeverLandsIsEventuallyAbandoned(Env env) { PursuitHarness.SearchGate gate = new PursuitHarness.SearchGate(); Instance instance = loadedGatedFlatInstance(env, gate); - EntityCreature actor = new EntityCreature(EntityType.ZOMBIE); - actor.setInstance(instance, new Pos(0.5, SURFACE_Y, 0.5)).join(); + EntityCreature actor = TestWorlds.spawn(instance, EntityType.ZOMBIE, + new Pos(0.5, SURFACE_Y, 0.5)); for (int settle = 0; settle < 3; settle++) env.tick(); NavigationSystem system = NavigationSystem.builder() @@ -200,8 +201,8 @@ private record Pursuit( private static Pursuit pursue(Env env, int latencyTicks, int retargetTicks) { PursuitHarness.SearchGate gate = new PursuitHarness.SearchGate(); Instance instance = loadedGatedFlatInstance(env, gate); - EntityCreature actor = new EntityCreature(EntityType.ZOMBIE); - actor.setInstance(instance, new Pos(0.5, SURFACE_Y, 0.5)).join(); + EntityCreature actor = TestWorlds.spawn(instance, EntityType.ZOMBIE, + new Pos(0.5, SURFACE_Y, 0.5)); for (int settle = 0; settle < 3; settle++) env.tick(); NavigationSystem system = NavigationSystem.builder() @@ -255,5 +256,4 @@ private static Pursuit pursue(Env env, int latencyTicks, int retargetTicks) { env.destroyInstance(instance); } } - } diff --git a/src/test/java/ca/atlasengine/pathfinding/CornerCutPolicyTest.java b/src/test/java/ca/atlasengine/pathfinding/CornerCutPolicyTest.java index 675fda5..9298b51 100644 --- a/src/test/java/ca/atlasengine/pathfinding/CornerCutPolicyTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/CornerCutPolicyTest.java @@ -3,21 +3,15 @@ import ca.atlasengine.pathfinding.internal.movement.RouteGeometry; import ca.atlasengine.pathfinding.profile.NavigationMode; import ca.atlasengine.pathfinding.terrain.TerrainType; -import net.minestom.server.entity.EntityType; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; class CornerCutPolicyTest { @Test - void everyMobRetainsShorelineWaypoints() { - for (EntityType type : new EntityType[]{ - EntityType.ZOMBIE, EntityType.FROG, EntityType.AXOLOTL - }) { - assertFalse(RouteGeometry.permitsCornerCut( - type, TerrainType.WATER_BORDER)); - } + void shorelineWaypointsAreRetained() { + assertFalse(RouteGeometry.permitsCornerCut(TerrainType.WATER_BORDER)); } @Test @@ -27,8 +21,7 @@ void protectedHazardAndDoorNodesCannotBeCut() { TerrainType.DAMAGING_IN_NEIGHBOR, TerrainType.WALKABLE_DOOR }) { - assertFalse(RouteGeometry.permitsCornerCut( - EntityType.ZOMBIE, type)); + assertFalse(RouteGeometry.permitsCornerCut(type)); } } diff --git a/src/test/java/ca/atlasengine/pathfinding/CropTraversalE2ETest.java b/src/test/java/ca/atlasengine/pathfinding/CropTraversalE2ETest.java index 660e06b..de8d35b 100644 --- a/src/test/java/ca/atlasengine/pathfinding/CropTraversalE2ETest.java +++ b/src/test/java/ca/atlasengine/pathfinding/CropTraversalE2ETest.java @@ -1,10 +1,8 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.result.PathNode; import ca.atlasengine.pathfinding.terrain.BlockTraversalData; import ca.atlasengine.pathfinding.terrain.TerrainClassifier; import ca.atlasengine.pathfinding.terrain.TerrainType; -import net.minestom.server.coordinate.ChunkRange; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.EntityCreature; import net.minestom.server.entity.EntityType; @@ -61,8 +59,8 @@ void everyCropIsOpenGroundRatherThanABlockedCell() { @Test void aMobWalksThroughAPlantedFieldInsteadOfStopping(Env env) { Instance instance = plantedField(env, Block.WHEAT); - EntityCreature mob = new EntityCreature(EntityType.ZOMBIE); - mob.setInstance(instance, new Pos(0.5, 41, 0.5)).join(); + EntityCreature mob = TestWorlds.spawn(instance, EntityType.ZOMBIE, + new Pos(0.5, 41, 0.5)); env.tick(); try (NavigationSystem navigation = NavigationSystem.builder() @@ -86,8 +84,8 @@ void aMobWalksThroughAPlantedFieldInsteadOfStopping(Env env) { void aMobSpawnedInsideCropsStillMoves(Env env) { // The exact report in #3224: spawned into the crop, never moves. Instance instance = plantedField(env, Block.CARROTS); - EntityCreature mob = new EntityCreature(EntityType.ZOMBIE); - mob.setInstance(instance, new Pos(4.5, 41, 0.5)).join(); + EntityCreature mob = TestWorlds.spawn(instance, EntityType.ZOMBIE, + new Pos(4.5, 41, 0.5)); env.tick(); try (NavigationSystem navigation = NavigationSystem.builder() @@ -110,8 +108,8 @@ void aMobSpawnedInsideCropsStillMoves(Env env) { void aRouteIsFoundThroughEveryCropFamily(Env env) { for (Block crop : CROPS) { Instance instance = plantedField(env, crop); - EntityCreature mob = new EntityCreature(EntityType.ZOMBIE); - mob.setInstance(instance, new Pos(0.5, 41, 0.5)).join(); + EntityCreature mob = TestWorlds.spawn(instance, EntityType.ZOMBIE, + new Pos(0.5, 41, 0.5)); env.tick(); try (NavigationSystem navigation = NavigationSystem.builder() .parallelism(1).queueCapacity(8).build()) { @@ -141,9 +139,7 @@ void aRouteIsFoundThroughEveryCropFamily(Env env) { /** Soil with a crop planted on top, the shape the reports show. */ private static Instance plantedField(Env env, Block crop) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 2, - (x, z) -> instance.loadChunk(x, z).join()); + Instance instance = TestWorlds.loadedFlat(env, 2); for (int x = -2; x <= 14; x++) { for (int z = -3; z <= 3; z++) { instance.setBlock(x, 40, z, Block.DIRT); diff --git a/src/test/java/ca/atlasengine/pathfinding/DoorRailTraversalTest.java b/src/test/java/ca/atlasengine/pathfinding/DoorRailTraversalTest.java index d8c3811..9b7dda8 100644 --- a/src/test/java/ca/atlasengine/pathfinding/DoorRailTraversalTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/DoorRailTraversalTest.java @@ -1,6 +1,13 @@ 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.PathResult;import ca.atlasengine.pathfinding.search.EntityPathfinder;import ca.atlasengine.pathfinding.search.NavigationRequest;import ca.atlasengine.pathfinding.search.SearchControl;import ca.atlasengine.pathfinding.terrain.BlockManipulationCapabilities; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +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.BlockManipulationCapabilities; import ca.atlasengine.pathfinding.terrain.MobTraversalProfile; import ca.atlasengine.pathfinding.terrain.OpenableBlockFamily; import ca.atlasengine.pathfinding.terrain.TerrainType; diff --git a/src/test/java/ca/atlasengine/pathfinding/EveryBuiltinMobE2ETest.java b/src/test/java/ca/atlasengine/pathfinding/EveryBuiltinMobE2ETest.java index 260fb4c..4d1fda4 100644 --- a/src/test/java/ca/atlasengine/pathfinding/EveryBuiltinMobE2ETest.java +++ b/src/test/java/ca/atlasengine/pathfinding/EveryBuiltinMobE2ETest.java @@ -4,7 +4,6 @@ import ca.atlasengine.pathfinding.profile.NavigationMode; import ca.atlasengine.pathfinding.profile.NavigationProfile; import ca.atlasengine.pathfinding.result.PathNode; -import net.minestom.server.coordinate.ChunkRange; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.EntityCreature; import net.minestom.server.entity.EntityType; @@ -84,7 +83,7 @@ private static void assertCompletes( TrackedCreature mob = new TrackedCreature(type); mob.setInstance(instance, start).join(); EntityNavigationController controller = - new EntityNavigationController(mob, service, profile, 0.2); + TestWorlds.controller(mob, service, profile, 0.2); mob.controller = controller; controller.moveTo(goal); @@ -183,7 +182,6 @@ private static void prepareCourses(Instance instance) { instance.setBlock(x, 40, z, lowerSlab); } } - } private static void assertNonTrivialRoute( @@ -225,9 +223,7 @@ private static void runUntilTerminal( } private static Instance prepared(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 5, - (x, z) -> instance.loadChunk(x, z).join()); + Instance instance = TestWorlds.loadedFlat(env, 5); return instance; } diff --git a/src/test/java/ca/atlasengine/pathfinding/FloatingGroundNavigationTest.java b/src/test/java/ca/atlasengine/pathfinding/FloatingGroundNavigationTest.java index 0161c6b..a77e759 100644 --- a/src/test/java/ca/atlasengine/pathfinding/FloatingGroundNavigationTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/FloatingGroundNavigationTest.java @@ -1,12 +1,21 @@ 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.result.PathResult;import ca.atlasengine.pathfinding.search.EntityPathfinder;import ca.atlasengine.pathfinding.search.EntityTraversalState;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.result.PathResult; +import ca.atlasengine.pathfinding.search.EntityPathfinder; +import ca.atlasengine.pathfinding.search.EntityTraversalState; +import ca.atlasengine.pathfinding.search.NavigationRequest; +import ca.atlasengine.pathfinding.search.SearchControl; +import ca.atlasengine.pathfinding.terrain.MobTraversalProfile; import ca.atlasengine.pathfinding.terrain.TerrainType; import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Pos; import net.minestom.server.instance.block.Block; import org.junit.jupiter.api.Test; +import java.util.List; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -71,14 +80,16 @@ void floatingStartIncludesWaterloggedSourceCellAboveWater() { .canFloat(true) .build(); - NavigationProfile profile = NavigationProfile.builder(NavigationMode.GROUND, floating, GroundCapabilities.STANDARD).allowBreaching(false).prefersShallowWater(false).avoidSun(false).build(); + NavigationProfile profile = NavigationProfile.builder(NavigationMode.GROUND, floating, + GroundCapabilities.STANDARD).build(); PathResult result = pathfinder.findPath( NavigationRequest.builder( world, new Pos(0.5, 0.2, 0.5), new Pos(2.5, 1, 0.5), MOB, profile) .maxPathLength(12) .entityState(new EntityTraversalState( - false, true, Set.of(), -64)) + false, true, Set.of(), -64, + EntityTraversalState.OVERWORLD_MAX_BUILD_HEIGHT, List.of())) .build(), SearchControl.NONE); @@ -105,13 +116,15 @@ void nonFloatingWalkerCollapsesWaterCandidateToPoolBottom() { private PathResult find(TestWorld world, MobTraversalProfile profile, Pos start, Pos goal) { - NavigationProfile navigation = NavigationProfile.builder(NavigationMode.GROUND, profile, GroundCapabilities.STANDARD).allowBreaching(false).prefersShallowWater(false).avoidSun(false).build(); + NavigationProfile navigation = NavigationProfile.builder(NavigationMode.GROUND, profile, + GroundCapabilities.STANDARD).build(); return pathfinder.findPath(NavigationRequest.builder( world, start, goal, MOB, navigation) .maxPathLength(32) .maxVisitedMultiplier(8) .entityState(new EntityTraversalState( - false, true, Set.of(), -64)) + false, true, Set.of(), -64, + EntityTraversalState.OVERWORLD_MAX_BUILD_HEIGHT, List.of())) .build(), SearchControl.NONE); } diff --git a/src/test/java/ca/atlasengine/pathfinding/GeneratedNavigatorE2ETest.java b/src/test/java/ca/atlasengine/pathfinding/GeneratedNavigatorE2ETest.java index 6f57773..29d3377 100644 --- a/src/test/java/ca/atlasengine/pathfinding/GeneratedNavigatorE2ETest.java +++ b/src/test/java/ca/atlasengine/pathfinding/GeneratedNavigatorE2ETest.java @@ -3,7 +3,6 @@ import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; import ca.atlasengine.pathfinding.profile.NavigationMode; import ca.atlasengine.pathfinding.result.PathNode; -import net.minestom.server.coordinate.ChunkRange; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.EntityCreature; import net.minestom.server.entity.EntityType; @@ -420,7 +419,7 @@ void concurrentRetargetingUnderSingleWorkerRemainsLive(Env env) throws Exception index % 2 == 0 ? EntityType.ZOMBIE : EntityType.SILVERFISH); mob.setInstance(instance, new Pos(0.5, 40, z)).join(); EntityNavigationController controller = - EntityNavigationController.builtin(mob, service, 0.18); + TestWorlds.builtinController(mob, service, 0.18); mob.controller = controller; controller.moveTo(new Pos(12.5, 40, z)); mobs.add(mob); @@ -477,7 +476,7 @@ void saturatedControllersSurviveRemovalRetargetAndBlockChange(Env env) TrackedCreature mob = new TrackedCreature(EntityType.SILVERFISH); mob.setInstance(instance, new Pos(0.5, 40, z)).join(); EntityNavigationController controller = - EntityNavigationController.builtin(mob, service, 0.2); + TestWorlds.builtinController(mob, service, 0.2); mob.controller = controller; controller.moveTo(new Pos(13.5, 40, z)); mobs.add(mob); @@ -589,7 +588,7 @@ private static void runShapedSpatialCase( try { mob.setInstance(instance, start).join(); EntityNavigationController controller = - EntityNavigationController.builtin(mob, service, 0.18); + TestWorlds.builtinController(mob, service, 0.18); mob.controller = controller; controller.moveTo(goal); runUntilTerminal(env, controller, 2_000); @@ -623,7 +622,7 @@ private static void runCase( try { mob.setInstance(instance, start).join(); EntityNavigationController controller = - EntityNavigationController.builtin(mob, service, 0.18); + TestWorlds.builtinController(mob, service, 0.18); mob.controller = controller; assertEquals(expectedMode, BuiltinNavigationProfiles .forEntityType(type).mode(), label); @@ -655,7 +654,7 @@ private static void runMutationCase( try { mob.setInstance(instance, start).join(); EntityNavigationController controller = - EntityNavigationController.builtin(mob, service, 0.18); + TestWorlds.builtinController(mob, service, 0.18); mob.controller = controller; controller.moveTo(goal); for (int tick = 0; tick < 300 @@ -700,7 +699,7 @@ private static void runShapedCase( try { mob.setInstance(instance, start).join(); EntityNavigationController controller = - EntityNavigationController.builtin(mob, service, 0.18); + TestWorlds.builtinController(mob, service, 0.18); mob.controller = controller; controller.moveTo(goal); runUntilTerminal(env, controller, 2_000); @@ -722,7 +721,7 @@ private static void runMultiMutationCase( try { mob.setInstance(instance, new Pos(0.5, 40, 0.5)).join(); EntityNavigationController controller = - EntityNavigationController.builtin(mob, service, 0.18); + TestWorlds.builtinController(mob, service, 0.18); mob.controller = controller; controller.moveTo(new Pos(13.5, 40, 0.5)); for (int tick = 0; tick < 2_000; tick++) { @@ -765,7 +764,7 @@ private static void runStatefulSchedule( try { mob.setInstance(instance, new Pos(0.5, 40, 0.5)).join(); EntityNavigationController controller = - EntityNavigationController.builtin(mob, service, 0.2); + TestWorlds.builtinController(mob, service, 0.2); mob.controller = controller; controller.moveTo(new Pos(13.5, 40, 0.5)); for (int tick = 0; tick < 2_500; tick++) { @@ -917,9 +916,7 @@ private static long seed(int family, int index) { } private static Instance prepared(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 3, - (x, z) -> instance.loadChunk(x, z).join()); + Instance instance = TestWorlds.loadedFlat(env, 3); return instance; } diff --git a/src/test/java/ca/atlasengine/pathfinding/GroundNodePathfinderTest.java b/src/test/java/ca/atlasengine/pathfinding/GroundNodePathfinderTest.java index f3bc431..b67b7fb 100644 --- a/src/test/java/ca/atlasengine/pathfinding/GroundNodePathfinderTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/GroundNodePathfinderTest.java @@ -1,18 +1,31 @@ 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.NavigationProfile;import ca.atlasengine.pathfinding.profile.PlatformJumpCapabilities;import ca.atlasengine.pathfinding.result.PathNode;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.result.PathStatus;import ca.atlasengine.pathfinding.search.DiscreteGroundPathfinder;import ca.atlasengine.pathfinding.search.EntityPathfinder;import ca.atlasengine.pathfinding.search.EntityTraversalState;import ca.atlasengine.pathfinding.search.GroundSearchLimits;import ca.atlasengine.pathfinding.search.NavigationRequest;import ca.atlasengine.pathfinding.search.SearchControl;import ca.atlasengine.pathfinding.influence.InfluenceResult; +import ca.atlasengine.pathfinding.influence.InfluenceResult; import ca.atlasengine.pathfinding.influence.NavigationInfluence; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.ClimbableCapabilities; +import ca.atlasengine.pathfinding.profile.GroundCapabilities; +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.result.PathStatus; +import ca.atlasengine.pathfinding.search.EntityPathfinder; +import ca.atlasengine.pathfinding.search.EntityTraversalState; +import ca.atlasengine.pathfinding.search.NavigationRequest; +import ca.atlasengine.pathfinding.search.SearchControl; 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.Pos; import net.minestom.server.coordinate.Vec; -import net.minestom.server.instance.block.Block; import net.minestom.server.entity.EntityType; +import net.minestom.server.instance.block.Block; import org.junit.jupiter.api.Test; import java.util.List; +import java.util.Set; import static org.junit.jupiter.api.Assertions.*; @@ -23,8 +36,6 @@ class GroundNodePathfinderTest { private static final BoundingBox STANDARD = new BoundingBox(0.6, 1.8, 0.6); - private final DiscreteGroundPathfinder pathfinder = - new DiscreteGroundPathfinder(); @Test void climbableRoutesAreExplicitlyOptInAndEmitVerticalClimbNodes() { @@ -34,11 +45,16 @@ void climbableRoutesAreExplicitlyOptInAndEmitVerticalClimbNodes() { GroundCapabilities climbing = GroundCapabilities.STANDARD .withClimbables(ClimbableCapabilities.STANDARD); GroundCapabilities climbingWithoutLongFalls = - GroundCapabilities.builder().maxStepHeight(1).maxFallDistance(1).allowDiagonal(true).horizontalEdgeCost(false).platformJump(PlatformJumpCapabilities.DISABLED).climbables(ClimbableCapabilities.STANDARD).build(); + GroundCapabilities.builder() + .maxFallDistance(1) + .climbables(ClimbableCapabilities.STANDARD) + .build(); PathResult baselineGround = find(world, start, top, STANDARD, 16); - PathResult ascent = pathfinder.findPath(world, start, top, STANDARD, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(16).reachRange(0).maxVisitedMultiplier(8).build(), climbing, SearchControl.NONE); - PathResult descent = pathfinder.findPath(world, top, start, STANDARD, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(16).reachRange(0).maxVisitedMultiplier(8).build(), climbingWithoutLongFalls, SearchControl.NONE); + PathResult ascent = GroundSearches.find(world, start, top, STANDARD, + MobTraversalProfile.DEFAULT, 16, 8, climbing, SearchControl.NONE); + PathResult descent = GroundSearches.find(world, top, start, STANDARD, + MobTraversalProfile.DEFAULT, 16, 8, climbingWithoutLongFalls, SearchControl.NONE); assertFalse(baselineGround.found(), "ordinary baseline ground profiles must not invent ladder edges"); @@ -56,7 +72,9 @@ void ladderRouteOffsetsBoundingBoxOutsideTheClimbFace() { GroundCapabilities climbing = GroundCapabilities.STANDARD .withClimbables(ClimbableCapabilities.STANDARD); - PathResult result = pathfinder.findPath(world, new Pos(-0.5, 1, 0.5), new Pos(1.5, 5, 0.5), new BoundingBox(1.8, 1.8, 0.8), MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(16).reachRange(0).maxVisitedMultiplier(8).build(), climbing, SearchControl.NONE); + PathResult result = GroundSearches.find(world, new Pos(-0.5, 1, 0.5), new Pos(1.5, 5, 0.5), + new BoundingBox(1.8, 1.8, 0.8), MobTraversalProfile.DEFAULT, 16, 8, climbing, + SearchControl.NONE); assertTrue(result.found(), result::toString); assertTrue(result.nodes().stream() @@ -89,7 +107,9 @@ void completeMinecraftClimbableTagCanProduceOptInVerticalEdges() { } } - PathResult result = pathfinder.findPath(world, new Pos(0.5, 1, 0.5), new Pos(1.5, 5, 0.5), STANDARD, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(16).reachRange(0).maxVisitedMultiplier(8).build(), climbing, SearchControl.NONE); + PathResult result = GroundSearches.find(world, new Pos(0.5, 1, 0.5), + new Pos(1.5, 5, 0.5), STANDARD, MobTraversalProfile.DEFAULT, 16, 8, climbing, + SearchControl.NONE); assertTrue(BlockTraversalData.isClimbable(state), id); assertTrue(result.found(), () -> id + ": " + result); @@ -104,16 +124,26 @@ void climbDirectionsAndInterruptedColumnsAreHardConstraints() { Pos bottom = new Pos(0.5, 1, 0.5); Pos top = new Pos(1.5, 5, 0.5); GroundCapabilities ascentOnly = GroundCapabilities.STANDARD - .withClimbables(ClimbableCapabilities.builder().enabled(true).allowAscending(true).allowDescending(false).verticalCostMultiplier(1).build()); + .withClimbables(ClimbableCapabilities.ascendOnly(1)); GroundCapabilities ascentOnlyWithoutLongFalls = - GroundCapabilities.builder().maxStepHeight(1).maxFallDistance(1).allowDiagonal(true).horizontalEdgeCost(false).platformJump(PlatformJumpCapabilities.DISABLED).climbables(ClimbableCapabilities.builder().enabled(true).allowAscending(true).allowDescending(false).verticalCostMultiplier(1).build()).build(); - GroundCapabilities descentOnly = GroundCapabilities.builder().maxStepHeight(1).maxFallDistance(1).allowDiagonal(true).horizontalEdgeCost(false).platformJump(PlatformJumpCapabilities.DISABLED).climbables(ClimbableCapabilities.builder().enabled(true).allowAscending(false).allowDescending(true).verticalCostMultiplier(1).build()).build(); + GroundCapabilities.builder() + .maxFallDistance(1) + .climbables(ClimbableCapabilities.ascendOnly(1)) + .build(); + GroundCapabilities descentOnly = GroundCapabilities.builder() + .maxFallDistance(1) + .climbables(ClimbableCapabilities.descendOnly(1)) + .build(); - PathResult up = pathfinder.findPath(world, bottom, top, STANDARD, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(16).reachRange(0).maxVisitedMultiplier(8).build(), ascentOnly, SearchControl.NONE); - PathResult downRejected = pathfinder.findPath(world, top, bottom, STANDARD, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(16).reachRange(0).maxVisitedMultiplier(8).build(), ascentOnlyWithoutLongFalls, SearchControl.NONE); - PathResult down = pathfinder.findPath(world, top, bottom, STANDARD, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(16).reachRange(0).maxVisitedMultiplier(8).build(), descentOnly, SearchControl.NONE); + PathResult up = GroundSearches.find(world, bottom, top, STANDARD, + MobTraversalProfile.DEFAULT, 16, 8, ascentOnly, SearchControl.NONE); + PathResult downRejected = GroundSearches.find(world, top, bottom, STANDARD, + MobTraversalProfile.DEFAULT, 16, 8, ascentOnlyWithoutLongFalls, SearchControl.NONE); + PathResult down = GroundSearches.find(world, top, bottom, STANDARD, + MobTraversalProfile.DEFAULT, 16, 8, descentOnly, SearchControl.NONE); TestWorld broken = ladderTower(5).set(1, 3, 0, Block.AIR); - PathResult gap = pathfinder.findPath(broken, bottom, top, STANDARD, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(16).reachRange(0).maxVisitedMultiplier(8).build(), ascentOnly, SearchControl.NONE); + PathResult gap = GroundSearches.find(broken, bottom, top, STANDARD, + MobTraversalProfile.DEFAULT, 16, 8, ascentOnly, SearchControl.NONE); assertTrue(up.found(), up::toString); assertFalse(downRejected.found()); @@ -126,11 +156,15 @@ void climbDirectionsAndInterruptedColumnsAreHardConstraints() { void optInPlatformJumpCrossesOneCellGap() { TestWorld world = platformGap(2); GroundCapabilities jumper = GroundCapabilities.STANDARD - .withPlatformJump(PlatformJumpCapabilities.builder().maxHorizontalDistance(2).maxRise(0).maxDrop(0).apexClearance(1).build()); + .withPlatformJump(PlatformJumpCapabilities.builder() + .maxHorizontalDistance(2) + .apexClearance(1) + .build()); PathResult ordinary = find(world, new Pos(0.5, 1, 0.5), new Pos(2.5, 1, 0.5), STANDARD, 5); - PathResult jumping = pathfinder.findPath(world, new Pos(0.5, 1, 0.5), new Pos(2.5, 1, 0.5), STANDARD, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(5).reachRange(0).maxVisitedMultiplier(8).build(), jumper, SearchControl.NONE); + PathResult jumping = GroundSearches.find(world, new Pos(0.5, 1, 0.5), new Pos(2.5, 1, 0.5), + STANDARD, MobTraversalProfile.DEFAULT, 5, 8, jumper, SearchControl.NONE); assertFalse(ordinary.found()); assertTrue(jumping.found(), jumping::toString); @@ -138,7 +172,9 @@ void optInPlatformJumpCrossesOneCellGap() { assertEquals(PathNode.Movement.JUMP, jumping.nodes().getLast().movement()); - PathResult arcBudgetRejected = pathfinder.findPath(world, new Pos(0.5, 1, 0.5), new Pos(2.5, 1, 0.5), STANDARD, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(2.5).reachRange(0).maxVisitedMultiplier(8).build(), jumper, SearchControl.NONE); + PathResult arcBudgetRejected = GroundSearches.find(world, new Pos(0.5, 1, 0.5), + new Pos(2.5, 1, 0.5), STANDARD, MobTraversalProfile.DEFAULT, 2.5, 8, jumper, + SearchControl.NONE); assertFalse(arcBudgetRejected.found(), "maxPathLength must charge the jump arc, not only its chord"); } @@ -147,12 +183,20 @@ void optInPlatformJumpCrossesOneCellGap() { void configuredJumpDistanceIsAnInclusiveHardBoundary() { TestWorld world = platformGap(3); GroundCapabilities tooShort = GroundCapabilities.STANDARD - .withPlatformJump(PlatformJumpCapabilities.builder().maxHorizontalDistance(2).maxRise(0).maxDrop(0).apexClearance(1).build()); + .withPlatformJump(PlatformJumpCapabilities.builder() + .maxHorizontalDistance(2) + .apexClearance(1) + .build()); GroundCapabilities exact = GroundCapabilities.STANDARD - .withPlatformJump(PlatformJumpCapabilities.builder().maxHorizontalDistance(3).maxRise(0).maxDrop(0).apexClearance(1).build()); + .withPlatformJump(PlatformJumpCapabilities.builder() + .maxHorizontalDistance(3) + .apexClearance(1) + .build()); - PathResult rejected = pathfinder.findPath(world, new Pos(0.5, 1, 0.5), new Pos(3.5, 1, 0.5), STANDARD, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(6).reachRange(0).maxVisitedMultiplier(8).build(), tooShort, SearchControl.NONE); - PathResult accepted = pathfinder.findPath(world, new Pos(0.5, 1, 0.5), new Pos(3.5, 1, 0.5), STANDARD, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(6).reachRange(0).maxVisitedMultiplier(8).build(), exact, SearchControl.NONE); + PathResult rejected = GroundSearches.find(world, new Pos(0.5, 1, 0.5), new Pos(3.5, 1, 0.5), + STANDARD, MobTraversalProfile.DEFAULT, 6, 8, tooShort, SearchControl.NONE); + PathResult accepted = GroundSearches.find(world, new Pos(0.5, 1, 0.5), new Pos(3.5, 1, 0.5), + STANDARD, MobTraversalProfile.DEFAULT, 6, 8, exact, SearchControl.NONE); assertFalse(rejected.found()); assertTrue(accepted.found(), accepted::toString); @@ -166,9 +210,19 @@ void configuredRiseAndDropAreInclusiveHardBoundaries() { TestWorld rise = platformGapWithLandingSupportY(2, 1); TestWorld drop = platformGapWithLandingSupportY(2, -1); GroundCapabilities levelOnly = GroundCapabilities.STANDARD - .withPlatformJump(PlatformJumpCapabilities.builder().maxHorizontalDistance(2).maxRise(0.99).maxDrop(0.99).apexClearance(2).build()); + .withPlatformJump(PlatformJumpCapabilities.builder() + .maxHorizontalDistance(2) + .maxRise(0.99) + .maxDrop(0.99) + .apexClearance(2) + .build()); GroundCapabilities oneBlock = GroundCapabilities.STANDARD - .withPlatformJump(PlatformJumpCapabilities.builder().maxHorizontalDistance(2).maxRise(1).maxDrop(1).apexClearance(2).build()); + .withPlatformJump(PlatformJumpCapabilities.builder() + .maxHorizontalDistance(2) + .maxRise(1) + .maxDrop(1) + .apexClearance(2) + .build()); PathResult riseRejected = jumpPath( rise, new Pos(0.5, 1, 0.5), @@ -200,9 +254,13 @@ void partialJumpPathEndsOnLastReachablePlatform() { world.column(x, 1, 0, 4, Block.STONE); } GroundCapabilities jumper = GroundCapabilities.STANDARD - .withPlatformJump(PlatformJumpCapabilities.builder().maxHorizontalDistance(2).maxRise(0).maxDrop(0).apexClearance(1).build()); + .withPlatformJump(PlatformJumpCapabilities.builder() + .maxHorizontalDistance(2) + .apexClearance(1) + .build()); - PathResult result = pathfinder.findPath(world, new Pos(0.5, 1, 0.5), new Pos(6.5, 1, 0.5), STANDARD, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(12).reachRange(0).maxVisitedMultiplier(8).build(), jumper, SearchControl.NONE); + PathResult result = GroundSearches.find(world, new Pos(0.5, 1, 0.5), new Pos(6.5, 1, 0.5), + STANDARD, MobTraversalProfile.DEFAULT, 12, 8, jumper, SearchControl.NONE); assertEquals(PathStatus.PARTIAL, result.status()); assertEquals(2, result.nodes().getLast().graphX()); @@ -217,9 +275,13 @@ void partialJumpPathEndsOnLastReachablePlatform() { void jumpCandidateChecksScaleLinearlyWithDistance() { TestWorld world = new TestWorld().set(0, 0, 0, Block.STONE); GroundCapabilities jumper = GroundCapabilities.STANDARD - .withPlatformJump(PlatformJumpCapabilities.builder().maxHorizontalDistance(32).maxRise(0).maxDrop(0).apexClearance(1).build()); + .withPlatformJump(PlatformJumpCapabilities.builder() + .maxHorizontalDistance(32) + .apexClearance(1) + .build()); - PathResult result = pathfinder.findPath(world, new Pos(0.5, 1, 0.5), new Pos(100.5, 1, 0.5), STANDARD, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(64).reachRange(0).maxVisitedMultiplier(8).build(), jumper, SearchControl.NONE); + PathResult result = GroundSearches.find(world, new Pos(0.5, 1, 0.5), new Pos(100.5, 1, 0.5), + STANDARD, MobTraversalProfile.DEFAULT, 64, 8, jumper, SearchControl.NONE); assertEquals(PathStatus.PARTIAL, result.status()); assertEquals(1, result.visitedNodes()); @@ -234,10 +296,16 @@ void platformJumpArcRejectsLowCeiling() { TestWorld ceiling = platformGap(2) .set(1, 3, 0, Block.STONE); GroundCapabilities jumper = GroundCapabilities.STANDARD - .withPlatformJump(PlatformJumpCapabilities.builder().maxHorizontalDistance(2).maxRise(0).maxDrop(0).apexClearance(1).build()); - - PathResult accepted = pathfinder.findPath(clear, new Pos(0.5, 1, 0.5), new Pos(2.5, 1, 0.5), STANDARD, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(5).reachRange(0).maxVisitedMultiplier(8).build(), jumper, SearchControl.NONE); - PathResult rejected = pathfinder.findPath(ceiling, new Pos(0.5, 1, 0.5), new Pos(2.5, 1, 0.5), STANDARD, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(5).reachRange(0).maxVisitedMultiplier(8).build(), jumper, SearchControl.NONE); + .withPlatformJump(PlatformJumpCapabilities.builder() + .maxHorizontalDistance(2) + .apexClearance(1) + .build()); + + PathResult accepted = GroundSearches.find(clear, new Pos(0.5, 1, 0.5), new Pos(2.5, 1, 0.5), + STANDARD, MobTraversalProfile.DEFAULT, 5, 8, jumper, SearchControl.NONE); + PathResult rejected = GroundSearches.find(ceiling, new Pos(0.5, 1, 0.5), + new Pos(2.5, 1, 0.5), STANDARD, MobTraversalProfile.DEFAULT, 5, 8, jumper, + SearchControl.NONE); assertTrue(accepted.found(), accepted::toString); assertFalse(rejected.found(), @@ -255,9 +323,15 @@ void platformJumpCapabilityDoesNotReplaceOrdinaryShapeEdges() { .withProperty("facing", "east") .withProperty("half", "bottom")); GroundCapabilities jumper = GroundCapabilities.STANDARD - .withPlatformJump(PlatformJumpCapabilities.builder().maxHorizontalDistance(4).maxRise(1).maxDrop(1).apexClearance(1).build()); + .withPlatformJump(PlatformJumpCapabilities.builder() + .maxHorizontalDistance(4) + .maxRise(1) + .maxDrop(1) + .apexClearance(1) + .build()); - PathResult result = pathfinder.findPath(world, new Pos(0.5, 1, 0.5), new Pos(5.5, 1, 0.5), STANDARD, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(10).reachRange(0).maxVisitedMultiplier(8).build(), jumper, SearchControl.NONE); + PathResult result = GroundSearches.find(world, new Pos(0.5, 1, 0.5), new Pos(5.5, 1, 0.5), + STANDARD, MobTraversalProfile.DEFAULT, 10, 8, jumper, SearchControl.NONE); assertTrue(result.found(), result::toString); assertTrue(result.nodes().stream().noneMatch(node -> @@ -342,7 +416,8 @@ void nonFloatingWalkerKeepsSingleSurfaceWaterCandidate() { .malus(TerrainType.WATER, 0) .build(); - PathResult result = pathfinder.findPath(world, new Pos(0.5, 1, 0.5), new Pos(1.5, 1, 0.5), STANDARD, profile, GroundSearchLimits.builder().maxPathLength(8).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.STANDARD, SearchControl.NONE); + PathResult result = GroundSearches.find(world, new Pos(0.5, 1, 0.5), new Pos(1.5, 1, 0.5), + STANDARD, profile, 8, 8, GroundCapabilities.STANDARD, SearchControl.NONE); assertTrue(result.found(), result::toString); assertEquals(1, result.nodes().getLast().graphX()); @@ -361,8 +436,12 @@ void fallDistanceBoundaryIsInclusiveAndNextBlockIsRejected() { world.column(x, 1, 1, 6, Block.STONE); } - PathResult exactBoundary = pathfinder.findPath(world, new Pos(0.5, 4, 0.5), new Pos(3.5, 2, 0.5), STANDARD, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(8).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.builder().maxStepHeight(1).maxFallDistance(2).allowDiagonal(true).build(), SearchControl.NONE); - PathResult oneTooFar = pathfinder.findPath(world, new Pos(0.5, 4, 0.5), new Pos(3.5, 2, 0.5), STANDARD, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(8).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.builder().maxStepHeight(1).maxFallDistance(1).allowDiagonal(true).build(), SearchControl.NONE); + PathResult exactBoundary = GroundSearches.find(world, new Pos(0.5, 4, 0.5), + new Pos(3.5, 2, 0.5), STANDARD, MobTraversalProfile.DEFAULT, 8, 8, + GroundCapabilities.builder().maxFallDistance(2).build(), SearchControl.NONE); + PathResult oneTooFar = GroundSearches.find(world, new Pos(0.5, 4, 0.5), + new Pos(3.5, 2, 0.5), STANDARD, MobTraversalProfile.DEFAULT, 8, 8, + GroundCapabilities.builder().maxFallDistance(1).build(), SearchControl.NONE); assertTrue(exactBoundary.found(), () -> exactBoundary.nodes().toString()); assertEquals(2, exactBoundary.nodes().get(1).blockY()); @@ -373,7 +452,9 @@ void fallDistanceBoundaryIsInclusiveAndNextBlockIsRejected() { @Test void subBlockStepCapabilityStillGetsOneRecursiveJumpNode() { TestWorld world = jumpCorridor(Block.STONE); - PathResult result = pathfinder.findPath(world, new Pos(1.5, 1, 0.5), new Pos(3.5, 1, 0.5), STANDARD, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(6).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.builder().maxStepHeight(0.5).maxFallDistance(5).allowDiagonal(true).build(), SearchControl.NONE); + PathResult result = GroundSearches.find(world, new Pos(1.5, 1, 0.5), new Pos(3.5, 1, 0.5), + STANDARD, MobTraversalProfile.DEFAULT, 6, 8, + GroundCapabilities.builder().maxStepHeight(0.5).build(), SearchControl.NONE); assertTrue(result.found(), () -> result.nodes().toString()); assertTrue(result.nodes().stream().anyMatch(node -> @@ -389,7 +470,8 @@ void explicitAirborneStateDescendsToFirstNonPathfindableSupport() { PathResult airborne = dispatch(world, new Pos(0.5, 4.2, 0.5), new Pos(3.5, 1, 0.5), zombie, - new EntityTraversalState(false, false, java.util.Set.of(), -64)); + new EntityTraversalState(false, false, Set.of(), -64, + EntityTraversalState.OVERWORLD_MAX_BUILD_HEIGHT, List.of())); PathResult claimedGrounded = dispatch(world, new Pos(0.5, 4.2, 0.5), new Pos(3.5, 1, 0.5), zombie, EntityTraversalState.GROUNDED); @@ -415,9 +497,11 @@ void explicitWaterAndFluidStandingStatesSelectColumnSurface() { } } EntityTraversalState swimming = new EntityTraversalState( - false, true, java.util.Set.of(), -64); + false, true, Set.of(), -64, EntityTraversalState.OVERWORLD_MAX_BUILD_HEIGHT, + List.of()); EntityTraversalState lavaStanding = new EntityTraversalState( - false, false, java.util.Set.of(Block.LAVA), -64); + false, false, Set.of(Block.LAVA), -64, + EntityTraversalState.OVERWORLD_MAX_BUILD_HEIGHT, List.of()); PathResult chicken = dispatch( water, new Pos(0.5, 1.2, 0.5), new Pos(3.5, 3, 0.5), @@ -493,8 +577,12 @@ void adjustedBudgetUsesDiscoveredPartialAndCanReturnStartOnlyPath() { TestWorld world = new TestWorld().floor( -3, 8, -3, 3, 0, Block.STONE); double oneExpansionBudget = 2.0 / (16 * 16); - PathResult discovered = pathfinder.findPath(world, new Pos(0.5, 1, 0.5), new Pos(5.5, 1, 0.5), STANDARD, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(12).reachRange(0).maxVisitedMultiplier(oneExpansionBudget).build(), GroundCapabilities.STANDARD, SearchControl.NONE); - PathResult startOnly = pathfinder.findPath(world, new Pos(0.5, 1, 0.5), new Pos(5.5, 1, 0.5), STANDARD, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(12).reachRange(0).maxVisitedMultiplier(1.0 / (16 * 16)).build(), GroundCapabilities.STANDARD, SearchControl.NONE); + PathResult discovered = GroundSearches.find(world, new Pos(0.5, 1, 0.5), + new Pos(5.5, 1, 0.5), STANDARD, MobTraversalProfile.DEFAULT, 12, oneExpansionBudget, + GroundCapabilities.STANDARD, SearchControl.NONE); + PathResult startOnly = GroundSearches.find(world, new Pos(0.5, 1, 0.5), + new Pos(5.5, 1, 0.5), STANDARD, MobTraversalProfile.DEFAULT, 12, 1.0 / (16 * 16), + GroundCapabilities.STANDARD, SearchControl.NONE); assertEquals(PathStatus.PARTIAL, discovered.status()); assertEquals(1, discovered.visitedNodes()); @@ -517,7 +605,9 @@ void invalidStartUsesActualAsymmetricBoundingBoxCorners() { BoundingBox offsetBox = new BoundingBox( 0.6, 1.8, 0.6, new Vec(0.3, 0, 0.3)); - PathResult result = pathfinder.findPath(world, new Pos(0.8, 1, 0.8), new Pos(4.5, 1, 1.5), offsetBox, MobTraversalProfile.DEFAULT, GroundSearchLimits.ofPathLength(12), GroundCapabilities.STANDARD, SearchControl.NONE); + PathResult result = GroundSearches.find(world, new Pos(0.8, 1, 0.8), new Pos(4.5, 1, 1.5), + offsetBox, MobTraversalProfile.DEFAULT, 12, 1, GroundCapabilities.STANDARD, + SearchControl.NONE); assertTrue(result.found(), result::toString); assertEquals(1, result.nodes().getFirst().graphX()); @@ -548,7 +638,9 @@ void disabledDiagonalCapabilityProducesOnlyCardinalEdges() { TestWorld world = new TestWorld().floor( -3, 8, -3, 8, 0, Block.STONE); - PathResult result = pathfinder.findPath(world, new Pos(0.5, 1, 0.5), new Pos(4.5, 1, 4.5), STANDARD, MobTraversalProfile.DEFAULT, GroundSearchLimits.ofPathLength(20), GroundCapabilities.builder().maxStepHeight(1).maxFallDistance(5).allowDiagonal(false).build(), SearchControl.NONE); + PathResult result = GroundSearches.find(world, new Pos(0.5, 1, 0.5), new Pos(4.5, 1, 4.5), + STANDARD, MobTraversalProfile.DEFAULT, 20, 1, + GroundCapabilities.builder().allowDiagonal(false).build(), SearchControl.NONE); assertTrue(result.found(), result::toString); for (int index = 1; index < result.nodes().size(); index++) { @@ -562,13 +654,15 @@ void disabledDiagonalCapabilityProducesOnlyCardinalEdges() { private PathResult find(TestWorld world, Pos start, Pos goal, BoundingBox box, double maxLength) { - return pathfinder.findPath(world, start, goal, box, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(maxLength).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.STANDARD, SearchControl.NONE); + return GroundSearches.find(world, start, goal, box, MobTraversalProfile.DEFAULT, maxLength, + 8, GroundCapabilities.STANDARD, SearchControl.NONE); } private PathResult jumpPath( TestWorld world, Pos start, Pos goal, BoundingBox box, GroundCapabilities capabilities) { - return pathfinder.findPath(world, start, goal, box, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(12).reachRange(0).maxVisitedMultiplier(8).build(), capabilities, SearchControl.NONE); + return GroundSearches.find(world, start, goal, box, MobTraversalProfile.DEFAULT, 12, 8, + capabilities, SearchControl.NONE); } private static PathResult dispatch( diff --git a/src/test/java/ca/atlasengine/pathfinding/GroundSearchContractTest.java b/src/test/java/ca/atlasengine/pathfinding/GroundSearchContractTest.java index 122f78d..4b51cd5 100644 --- a/src/test/java/ca/atlasengine/pathfinding/GroundSearchContractTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/GroundSearchContractTest.java @@ -1,6 +1,16 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.profile.GroundCapabilities;import ca.atlasengine.pathfinding.profile.NavigationProfile;import ca.atlasengine.pathfinding.result.PathNode;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.result.PathStatus;import ca.atlasengine.pathfinding.search.CancellationToken;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.GroundCapabilities; +import ca.atlasengine.pathfinding.profile.NavigationProfile; +import ca.atlasengine.pathfinding.result.PathNode; +import ca.atlasengine.pathfinding.result.PathResult; +import ca.atlasengine.pathfinding.result.PathStatus; +import ca.atlasengine.pathfinding.search.CancellationToken; +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.entity.EntityType; @@ -26,12 +36,10 @@ class GroundSearchContractTest { new BoundingBox(0.6, 1.8, 0.6); private static final double MAX_LENGTH = 32; private static final double VISITED_MULTIPLIER = 8; - /** {@link DiscreteGroundPathfinder}'s derived expansion budget. */ + /** The expansion budget a ground search derives from these limits. */ private static final int VISITED_BUDGET = (int) (Math.floor( Math.max(16, MAX_LENGTH) * 16) * VISITED_MULTIPLIER); - private final DiscreteGroundPathfinder pathfinder = - new DiscreteGroundPathfinder(); /** * A goal walled off on every side cannot be reached. The discrete search @@ -81,7 +89,9 @@ void expiredDeadlineStopsBeforeAnySearchWork() { SearchControl expired = new SearchControl( CancellationToken.NONE, System.nanoTime() - 1); - PathResult result = pathfinder.findPath(expiredWorld, new Pos(0.5, 1, 0.5), new Pos(5.5, 1, 0.5), STANDARD, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(MAX_LENGTH).reachRange(0).maxVisitedMultiplier(VISITED_MULTIPLIER).build(), GroundCapabilities.STANDARD, expired); + PathResult result = GroundSearches.find(expiredWorld, new Pos(0.5, 1, 0.5), + new Pos(5.5, 1, 0.5), STANDARD, MobTraversalProfile.DEFAULT, MAX_LENGTH, + VISITED_MULTIPLIER, GroundCapabilities.STANDARD, expired); assertEquals(PathStatus.TIMED_OUT, result.status()); assertEquals(0, result.visitedNodes()); @@ -124,22 +134,19 @@ void repeatedSearchIsDeterministic() { } /** - * One {@link EntityPathfinder} and one {@link DiscreteGroundPathfinder} - * serve sixty-four searches across eight threads. Neither holds - * search-scoped state, so every concurrent answer equals the answer the - * same instance gives alone. + * One {@link EntityPathfinder} serves sixty-four searches across eight + * threads. It holds no search-scoped state, so every concurrent answer + * equals the answer the same instance gives alone. */ @Test void onePathfinderInstanceServesManyConcurrentSearches() throws Exception { TestWorld world = flat(); EntityPathfinder entityPathfinder = new EntityPathfinder(); - DiscreteGroundPathfinder discrete = new DiscreteGroundPathfinder(); NavigationProfile zombie = BuiltinNavigationProfiles.forEntityType(EntityType.ZOMBIE); List sequential = new ArrayList<>(); for (int lane = -8; lane < 8; lane++) { sequential.add(entityRun(entityPathfinder, zombie, world, lane)); - sequential.add(discreteRun(discrete, world, lane)); } try (var executor = Executors.newFixedThreadPool(8)) { @@ -148,13 +155,12 @@ void onePathfinderInstanceServesManyConcurrentSearches() throws Exception { int lane = i % 16 - 8; tasks.add(() -> entityRun( entityPathfinder, zombie, world, lane)); - tasks.add(() -> discreteRun(discrete, world, lane)); } List observed = new ArrayList<>(); for (var future : executor.invokeAll(tasks)) { observed.add(future.get()); } - assertEquals(128, observed.size()); + assertEquals(64, observed.size()); for (int i = 0; i < observed.size(); i++) { PathResult actual = observed.get(i); PathResult reference = sequential.get(i % sequential.size()); @@ -197,7 +203,7 @@ void pathResultCollectionsAreImmutable() { assertTrue(result.found()); assertThrows(UnsupportedOperationException.class, () -> result.nodes().add( - new PathNode(0, 0, 0, PathNode.Movement.WALK))); + new PathNode(0, 0, 0, PathNode.Movement.WALK, 0, 0, 0))); assertThrows(UnsupportedOperationException.class, () -> result.nodes().clear()); assertThrows(UnsupportedOperationException.class, @@ -205,28 +211,32 @@ void pathResultCollectionsAreImmutable() { } /** - * A non-finite endpoint is a caller error, not a search: it is reported - * before the world is touched and before the budget is validated. + * A non-finite endpoint is a caller error, not a search: the request + * refuses it before the world is touched, as it does a zero budget. */ @Test void nonFiniteEndpointsAreRejectedBeforeAnyWorldRead() { TestWorld world = flat(); - assertEquals(PathStatus.INVALID_REQUEST, - find(world, new Pos(Double.NaN, 1, 0.5), - new Pos(2.5, 1, 0.5), STANDARD).status()); - assertEquals(PathStatus.INVALID_REQUEST, - find(world, new Pos(0.5, 1, 0.5), + assertThrows(IllegalArgumentException.class, + () -> find(world, new Pos(Double.NaN, 1, 0.5), + new Pos(2.5, 1, 0.5), STANDARD)); + assertThrows(IllegalArgumentException.class, + () -> find(world, new Pos(0.5, 1, 0.5), new Pos(2.5, Double.POSITIVE_INFINITY, 0.5), - STANDARD).status()); + STANDARD)); assertEquals(0, world.reads()); assertThrows(IllegalArgumentException.class, - () -> pathfinder.findPath(world, new Pos(0.5, 1, 0.5), new Pos(2.5, 1, 0.5), STANDARD, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(MAX_LENGTH).reachRange(0).maxVisitedMultiplier(0).build(), GroundCapabilities.STANDARD, SearchControl.NONE)); + () -> GroundSearches.find(world, new Pos(0.5, 1, 0.5), + new Pos(2.5, 1, 0.5), STANDARD, + MobTraversalProfile.DEFAULT, MAX_LENGTH, 0, + GroundCapabilities.STANDARD, SearchControl.NONE)); } private PathResult find(TestWorld world, Pos start, Pos goal, BoundingBox box) { - return pathfinder.findPath(world, start, goal, box, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(MAX_LENGTH).reachRange(0).maxVisitedMultiplier(VISITED_MULTIPLIER).build(), GroundCapabilities.STANDARD, SearchControl.NONE); + return GroundSearches.find(world, start, goal, box, MobTraversalProfile.DEFAULT, MAX_LENGTH, + VISITED_MULTIPLIER, GroundCapabilities.STANDARD, SearchControl.NONE); } private static PathResult entityRun( @@ -241,11 +251,6 @@ world, new Pos(-10.5, 1, lane + 0.5), SearchControl.NONE); } - private static PathResult discreteRun( - DiscreteGroundPathfinder pathfinder, TestWorld world, int lane) { - return pathfinder.findPath(world, new Pos(-10.5, 1, lane + 0.5), new Pos(10.5, 1, lane + 0.5), STANDARD, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(MAX_LENGTH).reachRange(0).maxVisitedMultiplier(VISITED_MULTIPLIER).build(), GroundCapabilities.STANDARD, SearchControl.NONE); - } - private static TestWorld flat() { return new TestWorld().floor(-24, 24, -24, 24, 0, Block.STONE); } diff --git a/src/test/java/ca/atlasengine/pathfinding/GroundSearches.java b/src/test/java/ca/atlasengine/pathfinding/GroundSearches.java new file mode 100644 index 0000000..201945c --- /dev/null +++ b/src/test/java/ca/atlasengine/pathfinding/GroundSearches.java @@ -0,0 +1,65 @@ +package ca.atlasengine.pathfinding; + +import ca.atlasengine.pathfinding.internal.Points; +import ca.atlasengine.pathfinding.profile.GroundCapabilities; +import ca.atlasengine.pathfinding.profile.NavigationMode; +import ca.atlasengine.pathfinding.profile.NavigationProfile; +import ca.atlasengine.pathfinding.result.PathResult; +import ca.atlasengine.pathfinding.search.EntityPathfinder; +import ca.atlasengine.pathfinding.search.EntityTraversalState; +import ca.atlasengine.pathfinding.search.NavigationRequest; +import ca.atlasengine.pathfinding.search.SearchControl; +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; + +/** + * One ground search between two exact cells, through the public + * {@link EntityPathfinder} entry point, for tests that pin a mob profile and + * capabilities rather than an entity type. + */ +public final class GroundSearches { + private static final EntityPathfinder PATHFINDER = new EntityPathfinder(); + + private GroundSearches() { + } + + /** + * Searches from {@code start} to {@code target}. The target is taken as + * the exact cell named, never projected onto a surface, and the entity is + * grounded, in water only if its start cell holds water. + * + * @param maxVisitedMultiplier scales the expansion budget of sixteen nodes + * per block of {@code maxPathLength} + */ + public static PathResult find(Block.Getter blocks, Point start, Point target, + BoundingBox box, MobTraversalProfile profile, + double maxPathLength, double maxVisitedMultiplier, + GroundCapabilities capabilities, + SearchControl control) { + NavigationProfile ground = NavigationProfile.builder( + NavigationMode.GROUND, profile, capabilities) + .pathToTargetsBelowSurface(true) + .build(); + // A request refuses non-finite endpoints itself; reading the world + // first would touch it on behalf of a request that never exists. + boolean inWater = Points.finite(start) && Points.finite(target) + && BlockTraversalData.hasWaterFluid( + blocks.getBlock(start, Block.Getter.Condition.TYPE)); + NavigationRequest request = NavigationRequest.builder( + blocks, start, target, box, ground) + .maxPathLength(maxPathLength) + .maxVisitedMultiplier(maxVisitedMultiplier) + .entityState(new EntityTraversalState(true, inWater, + profile.standableFluids(), + EntityTraversalState.OVERWORLD_MIN_BUILD_HEIGHT, + EntityTraversalState.OVERWORLD_MAX_BUILD_HEIGHT, + List.of())) + .build(); + return PATHFINDER.findPath(request, control); + } +} diff --git a/src/test/java/ca/atlasengine/pathfinding/GroundShapeClearanceTest.java b/src/test/java/ca/atlasengine/pathfinding/GroundShapeClearanceTest.java index f9a9700..59ffed1 100644 --- a/src/test/java/ca/atlasengine/pathfinding/GroundShapeClearanceTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/GroundShapeClearanceTest.java @@ -1,6 +1,10 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.GroundCapabilities;import ca.atlasengine.pathfinding.result.PathNode;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.search.DiscreteGroundPathfinder;import ca.atlasengine.pathfinding.search.GroundSearchLimits;import ca.atlasengine.pathfinding.search.SearchControl;import ca.atlasengine.pathfinding.terrain.MobTraversalProfile; +import ca.atlasengine.pathfinding.profile.GroundCapabilities; +import ca.atlasengine.pathfinding.result.PathNode; +import ca.atlasengine.pathfinding.result.PathResult; +import ca.atlasengine.pathfinding.search.SearchControl; +import ca.atlasengine.pathfinding.terrain.MobTraversalProfile; import net.minestom.server.collision.BoundingBox; import net.minestom.server.collision.CollisionUtils; import net.minestom.server.coordinate.Pos; @@ -22,8 +26,6 @@ class GroundShapeClearanceTest { private static final BoundingBox STANDARD = new BoundingBox(0.6, 1.8, 0.6); - private final DiscreteGroundPathfinder pathfinder = - new DiscreteGroundPathfinder(); @Test void zombieWidthFitsOneCellLedgeBetweenWaterAndTwoHighWall() { @@ -228,7 +230,7 @@ private static void assertLevelSegmentsClear( Vec start = previous.asVec().add(0, 1.0e-5, 0); Vec end = current.asVec().add(0, 1.0e-5, 0); var physics = CollisionUtils.handlePhysics( - world, box, start.asPos(), end.sub(start).asVec(), + world, box, start.asPos(), end.sub(start), null, false); assertFalse(physics.collisionX() || physics.collisionY() || physics.collisionZ(), @@ -238,7 +240,8 @@ private static void assertLevelSegmentsClear( private PathResult find(TestWorld world, Pos start, Pos goal, BoundingBox box, double maxLength) { - return pathfinder.findPath(world, start, goal, box, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(maxLength).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.STANDARD, SearchControl.NONE); + return GroundSearches.find(world, start, goal, box, MobTraversalProfile.DEFAULT, maxLength, + 8, GroundCapabilities.STANDARD, SearchControl.NONE); } private static TestWorld flat() { diff --git a/src/test/java/ca/atlasengine/pathfinding/HardMapOptimalityTest.java b/src/test/java/ca/atlasengine/pathfinding/HardMapOptimalityTest.java index 1d684d4..3d36465 100644 --- a/src/test/java/ca/atlasengine/pathfinding/HardMapOptimalityTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/HardMapOptimalityTest.java @@ -1,6 +1,10 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.GroundCapabilities;import ca.atlasengine.pathfinding.result.PathNode;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.search.DiscreteGroundPathfinder;import ca.atlasengine.pathfinding.search.GroundSearchLimits;import ca.atlasengine.pathfinding.search.SearchControl;import ca.atlasengine.pathfinding.terrain.MobTraversalProfile; +import ca.atlasengine.pathfinding.profile.GroundCapabilities; +import ca.atlasengine.pathfinding.result.PathNode; +import ca.atlasengine.pathfinding.result.PathResult; +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; @@ -42,9 +46,10 @@ void groundSearchMatchesIndependentShortestRouteForMultipleBoundingBoxes() { double expected = dijkstra(walls, start, goal, volume); assertTrue(Double.isFinite(expected), "oracle fixture has no route"); - PathResult result = new DiscreteGroundPathfinder().findPath(world, new Pos(start.x + volume * 0.5, 1, + PathResult result = GroundSearches.find(world, new Pos(start.x + volume * 0.5, 1, start.z + volume * 0.5), new Pos(goal.x + volume * 0.5, 1, - goal.z + volume * 0.5), box, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(160).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.STANDARD, SearchControl.NONE); + goal.z + volume * 0.5), box, MobTraversalProfile.DEFAULT, 160, 8, + GroundCapabilities.STANDARD, SearchControl.NONE); assertTrue(result.found(), () -> "width=" + width + ", nodes=" + result.nodes()); diff --git a/src/test/java/ca/atlasengine/pathfinding/HeadClearanceTest.java b/src/test/java/ca/atlasengine/pathfinding/HeadClearanceTest.java index 6035a81..69ea10f 100644 --- a/src/test/java/ca/atlasengine/pathfinding/HeadClearanceTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/HeadClearanceTest.java @@ -1,6 +1,13 @@ 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.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.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; @@ -200,7 +207,8 @@ world, new Pos(anchor, SURFACE, anchor), } private static NavigationProfile navigation() { - return NavigationProfile.builder(NavigationMode.GROUND, MobTraversalProfile.DEFAULT, GroundCapabilities.STANDARD).allowBreaching(false).prefersShallowWater(false).avoidSun(false).build(); + return NavigationProfile.builder(NavigationMode.GROUND, MobTraversalProfile.DEFAULT, + GroundCapabilities.STANDARD).build(); } private static Block trapdoor(String facing, String half, String open) { diff --git a/src/test/java/ca/atlasengine/pathfinding/InfluenceEqualityTest.java b/src/test/java/ca/atlasengine/pathfinding/InfluenceEqualityTest.java index bfb489e..cdafade 100644 --- a/src/test/java/ca/atlasengine/pathfinding/InfluenceEqualityTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/InfluenceEqualityTest.java @@ -1,13 +1,25 @@ 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.profile.PlatformJumpCapabilities;import ca.atlasengine.pathfinding.result.PathNode;import ca.atlasengine.pathfinding.result.PathStatus;import ca.atlasengine.pathfinding.search.EntityTraversalState;import ca.atlasengine.pathfinding.search.NavigationRequest;import ca.atlasengine.pathfinding.search.SearchControl;import ca.atlasengine.pathfinding.adaptive.SharedMeshPolicy; -import ca.atlasengine.pathfinding.internal.adaptive.SharedMeshCoordinator; -import ca.atlasengine.pathfinding.adaptive.SharedMeshSource; import ca.atlasengine.pathfinding.NavigationPlan; +import ca.atlasengine.pathfinding.adaptive.SharedMeshPolicy; +import ca.atlasengine.pathfinding.adaptive.SharedMeshSource; import ca.atlasengine.pathfinding.influence.EntityFearInfluence; import ca.atlasengine.pathfinding.influence.EntitySnapshot; import ca.atlasengine.pathfinding.influence.InfluenceResult; import ca.atlasengine.pathfinding.influence.NavigationInfluence; +import ca.atlasengine.pathfinding.internal.adaptive.SharedMeshCoordinator; +import ca.atlasengine.pathfinding.metrics.NavigationMetrics; +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.profile.PlatformJumpCapabilities; +import ca.atlasengine.pathfinding.result.PathNode; +import ca.atlasengine.pathfinding.result.PathStatus; +import ca.atlasengine.pathfinding.search.EntityTraversalState; +import ca.atlasengine.pathfinding.search.NavigationRequest; +import ca.atlasengine.pathfinding.search.SearchControl; import ca.atlasengine.pathfinding.terrain.BlockManipulationCapabilities; import ca.atlasengine.pathfinding.terrain.MobTraversalProfile; import ca.atlasengine.pathfinding.terrain.OpenableBlockFamily; @@ -197,7 +209,7 @@ private static NavigationProfile profile() { .build(); return NavigationProfile.builder(NavigationMode.GROUND, mob, GroundCapabilities.STANDARD .withPlatformJump(PlatformJumpCapabilities.acrossGaps(2)) - .withClimbables(ClimbableCapabilities.STANDARD)).allowBreaching(false).prefersShallowWater(false).avoidSun(false).build(); + .withClimbables(ClimbableCapabilities.STANDARD)).build(); } private record FearKey(double radius) {} @@ -242,7 +254,7 @@ private static SharedMeshSource promoteThirdActor( (actor, request) -> plan(request), SharedMeshPolicy.builder() .promotionRequests(2).retentionRequests(1) - .build()); + .build(), new NavigationMetrics()); TestWorld world = new TestWorld(); try { coordinator.request("seed-1", "target", 1, 0, @@ -277,6 +289,6 @@ private static CompletableFuture plan( new PathNode(target.x(), target.y(), target.z(), PathNode.Movement.WALK, target.blockX(), target.blockY(), target.blockZ())), - 2, 8)); + 2, 8, List.of())); } } diff --git a/src/test/java/ca/atlasengine/pathfinding/LiveInfluenceRoutingTest.java b/src/test/java/ca/atlasengine/pathfinding/LiveInfluenceRoutingTest.java index cb6a8a9..a1352bb 100644 --- a/src/test/java/ca/atlasengine/pathfinding/LiveInfluenceRoutingTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/LiveInfluenceRoutingTest.java @@ -1,10 +1,19 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.profile.GroundCapabilities;import ca.atlasengine.pathfinding.profile.NavigationMode;import ca.atlasengine.pathfinding.profile.NavigationProfile;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.search.EntityPathfinder;import ca.atlasengine.pathfinding.search.EntityTraversalState;import ca.atlasengine.pathfinding.search.NavigationRequest;import ca.atlasengine.pathfinding.search.SearchControl;import ca.atlasengine.pathfinding.influence.BlockAvoidanceInfluence; +import ca.atlasengine.pathfinding.influence.BlockAvoidanceInfluence; import ca.atlasengine.pathfinding.influence.EntityFearInfluence; import ca.atlasengine.pathfinding.influence.EntitySnapshot; import ca.atlasengine.pathfinding.influence.NavigationInfluence; import ca.atlasengine.pathfinding.influence.NavigationZoneInfluence; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.GroundCapabilities; +import ca.atlasengine.pathfinding.profile.NavigationMode; +import ca.atlasengine.pathfinding.profile.NavigationProfile; +import ca.atlasengine.pathfinding.result.PathResult; +import ca.atlasengine.pathfinding.search.EntityPathfinder; +import ca.atlasengine.pathfinding.search.EntityTraversalState; +import ca.atlasengine.pathfinding.search.NavigationRequest; +import ca.atlasengine.pathfinding.search.SearchControl; import ca.atlasengine.pathfinding.terrain.MobTraversalProfile; import ca.atlasengine.pathfinding.terrain.TerrainType; import net.minestom.server.collision.BoundingBox; @@ -137,14 +146,16 @@ private PathResult swim(TestWorld world, .malus(TerrainType.WATER, 0) .canFloat(true) .build(); - NavigationProfile profile = NavigationProfile.builder(NavigationMode.GROUND, floating, GroundCapabilities.STANDARD).allowBreaching(false).prefersShallowWater(false).avoidSun(false).build(); + NavigationProfile profile = NavigationProfile.builder(NavigationMode.GROUND, floating, + GroundCapabilities.STANDARD).build(); return pathfinder.findPath(NavigationRequest.builder( world, new Pos(0.5, 1.2, 0.5), new Pos(4.5, 3, 0.5), BOX, profile) .maxPathLength(32) .maxVisitedMultiplier(8) .entityState(new EntityTraversalState( - false, true, Set.of(), -64)) + false, true, Set.of(), -64, + EntityTraversalState.OVERWORLD_MAX_BUILD_HEIGHT, List.of())) .influences(influences) .build(), SearchControl.NONE); diff --git a/src/test/java/ca/atlasengine/pathfinding/MinestomNavigationE2ETest.java b/src/test/java/ca/atlasengine/pathfinding/MinestomNavigationE2ETest.java index 3cf0a14..b7f4ceb 100644 --- a/src/test/java/ca/atlasengine/pathfinding/MinestomNavigationE2ETest.java +++ b/src/test/java/ca/atlasengine/pathfinding/MinestomNavigationE2ETest.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.NavigationModifiers;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.EntityTraversalState;import ca.atlasengine.pathfinding.search.NavigationRequest;import ca.atlasengine.pathfinding.search.SearchControl;import ca.atlasengine.pathfinding.terrain.BlockManipulationCapabilities; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.ClimbableCapabilities; +import ca.atlasengine.pathfinding.profile.GroundCapabilities; +import ca.atlasengine.pathfinding.profile.NavigationModifiers; +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.EntityTraversalState; +import ca.atlasengine.pathfinding.search.NavigationRequest; +import ca.atlasengine.pathfinding.search.SearchControl; +import ca.atlasengine.pathfinding.terrain.BlockManipulationCapabilities; import ca.atlasengine.pathfinding.terrain.BlockTraversalData; import ca.atlasengine.pathfinding.terrain.OpenableBlockFamily; -import net.minestom.server.coordinate.ChunkRange; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.EntityCreature; import net.minestom.server.entity.EntityType; @@ -44,7 +55,7 @@ void jumpEnabledMobCrossesPlatformGapWithOneLaunch(Env env) { instance, EntityType.ZOMBIE, new Pos(0.5, 40, 0.5)); EntityNavigationController controller = - new EntityNavigationController( + TestWorlds.controller( mob, service, jumping, 0.18); mob.controller = controller; Pos goal = new Pos(4.5, 40, 0.5); @@ -95,7 +106,7 @@ void configuredFourBlockJumpCrossesThreeUnsupportedCells(Env env) { instance, EntityType.ZOMBIE, new Pos(0.5, 40, 0.5)); EntityNavigationController controller = - new EntityNavigationController( + TestWorlds.controller( mob, service, jumpProfile(4), 0.18); mob.controller = controller; controller.moveTo(new Pos(6.5, 40, 0.5)); @@ -138,7 +149,7 @@ void liveNavigationDetoursAroundDeepGroundPitWithoutFalling(Env env) { instance, EntityType.ZOMBIE, new Pos(0.5, 40, 0.5)); EntityNavigationController controller = - EntityNavigationController.builtin(mob, service, 0.24); + TestWorlds.builtinController(mob, service, 0.24); mob.controller = controller; controller.moveTo(new Pos(24.5, 40, 0.5)); @@ -195,7 +206,7 @@ void partialJumpPathLandsAndStopsOnReachableIsland(Env env) instance, EntityType.ZOMBIE, new Pos(0.5, 40, 0.5)); EntityNavigationController controller = - new EntityNavigationController( + TestWorlds.controller( mob, service, jumpProfile(2), 0.18); mob.controller = controller; controller.moveTo(new Pos(6.5, 40, 0.5)); @@ -256,7 +267,7 @@ void changedJumpArcIsRevalidatedBeforeLaunch(Env env) instance, EntityType.ZOMBIE, new Pos(0.5, 40, 0.5)); EntityNavigationController controller = - new EntityNavigationController( + TestWorlds.controller( mob, service, jumpProfile(2), 0.18); mob.controller = controller; controller.moveTo(new Pos(4.5, 40, 0.5)); @@ -323,7 +334,7 @@ void hardAlternatingMazeWorksForMultipleBoundingBoxes(Env env) { new Pos(0.5, 40, 0.5)); mob.setBoundingBox(width, 1.8, width); EntityNavigationController controller = - EntityNavigationController.builtin( + TestWorlds.builtinController( mob, service, 0.16); mob.controller = controller; controller.moveTo(new Pos(12.5, 40, 0.5)); @@ -380,7 +391,7 @@ void largeAlternatingTerrainCourseCompletesEfficientlyWithoutFalls( new Pos(0.5, 40, 0.5)); mob.setBoundingBox(1.8, 1.8, 0.8); EntityNavigationController controller = - EntityNavigationController.builtin(mob, service, 0.32); + TestWorlds.builtinController(mob, service, 0.32); mob.controller = controller; controller.moveTo(new Pos(60, 40, 0.5)); @@ -435,7 +446,7 @@ void largeMobCrossesUnevenTerracesWithoutSlidingIntoSideSlots(Env env) { new Pos(0.5, 40, 0.5)); mob.setBoundingBox(1.8, 2.7, 1.8); EntityNavigationController controller = - EntityNavigationController.builtin(mob, service, 0.22); + TestWorlds.builtinController(mob, service, 0.22); mob.controller = controller; controller.moveTo(new Pos(24.5, 40, 0.5)); @@ -483,7 +494,7 @@ void shapedTerrainAndHazardsWorkInLiveSimulation(Env env) { ControlledCreature mob = spawn( instance, EntityType.ZOMBIE, new Pos(0.5, 40, 0.5)); EntityNavigationController controller = - new EntityNavigationController( + TestWorlds.controller( mob, service, jumpProfile(4), 0.12); mob.controller = controller; controller.moveTo(new Pos(10.5, 40, 0.5)); @@ -559,7 +570,7 @@ void ordinaryStairFlightUsesStepPhysicsWithoutJumpImpulses(Env env) { ControlledCreature mob = spawn( instance, EntityType.ZOMBIE, new Pos(0.5, 40, 0.5)); EntityNavigationController controller = - EntityNavigationController.builtin(mob, service, 0.12); + TestWorlds.builtinController(mob, service, 0.12); mob.controller = controller; controller.moveTo(new Pos(9.5, 44, 0.5)); @@ -602,7 +613,7 @@ void customBoundingBoxesRespectExactOneByTwoOpening(Env env) { instance, EntityType.ZOMBIE, new Pos(0.5, 40, 0.5)); narrow.setBoundingBox(0.8, 1.8, 0.8); EntityNavigationController narrowController = - EntityNavigationController.builtin( + TestWorlds.builtinController( narrow, service, 0.14); narrow.controller = narrowController; narrowController.moveTo(new Pos(6.5, 40, 0.5)); @@ -620,7 +631,7 @@ void customBoundingBoxesRespectExactOneByTwoOpening(Env env) { instance, EntityType.ZOMBIE, new Pos(0.5, 40, 0.5)); wide.setBoundingBox(1.01, 1.8, 1.01); EntityNavigationController wideController = - EntityNavigationController.builtin(wide, service, 0.14); + TestWorlds.builtinController(wide, service, 0.14); wide.controller = wideController; wideController.moveTo(new Pos(6.5, 40, 0.5)); runUntilFinished(env, wideController, 1_000); @@ -646,7 +657,7 @@ void relevantBlockChangeRecomputesRouteAroundNewWall(Env env) ControlledCreature mob = spawn( instance, EntityType.ZOMBIE, new Pos(0.5, 40, 0.5)); EntityNavigationController controller = - EntityNavigationController.builtin(mob, service, 0.14); + TestWorlds.builtinController(mob, service, 0.14); mob.controller = controller; controller.moveTo(new Pos(8.5, 40, 0.5)); @@ -706,7 +717,7 @@ void exhaustedPartialPathStopsWithoutSelfResubmission(Env env) ControlledCreature mob = spawn( instance, EntityType.ZOMBIE, new Pos(0.5, 40, 0.5)); EntityNavigationController controller = - EntityNavigationController.builtin(mob, service, 0.18); + TestWorlds.builtinController(mob, service, 0.18); mob.controller = controller; controller.moveTo(new Pos(6.5, 40, 0.5)); @@ -753,7 +764,7 @@ void entityActuallyWalksAroundWallToGoal(Env env) { new AsyncEntityPathfindingService(2, 32)) { ControlledCreature mob = spawn(instance, EntityType.ZOMBIE, new Pos(0.5, 40, 0.5)); EntityNavigationController controller = - EntityNavigationController.builtin(mob, service, 0.18); + TestWorlds.builtinController(mob, service, 0.18); mob.controller = controller; controller.moveTo(new Pos(0.5, 40, 10.5)); @@ -801,7 +812,7 @@ void villagerOpensAndWalksThroughWoodDoor(Env env) { new AsyncEntityPathfindingService(1, 8)) { ControlledCreature mob = spawn(instance, EntityType.VILLAGER, new Pos(0.5, 40, 0.5)); EntityNavigationController controller = - EntityNavigationController.builtin(mob, service, 0.16); + TestWorlds.builtinController(mob, service, 0.16); mob.controller = controller; controller.moveTo(new Pos(5.5, 40, 0.5)); @@ -919,7 +930,7 @@ private static Instance walkThroughOpenable( instance, EntityType.VILLAGER, new Pos(0.5, 40, 0.5)); mob.watchedBlocks = watched; EntityNavigationController controller = - EntityNavigationController.builtin(mob, service, 0.16); + TestWorlds.builtinController(mob, service, 0.16); mob.controller = controller; controller.moveTo(new Pos(6.5, 40, 0.5), NavigationModifiers.builder() @@ -952,7 +963,7 @@ void fireNeighborWaypointCannotBeDirectionallySkipped(Env env) { ControlledCreature mob = spawn( instance, EntityType.ZOMBIE, new Pos(0.5, 40, 0.5)); EntityNavigationController controller = - EntityNavigationController.builtin(mob, service, 0.18); + TestWorlds.builtinController(mob, service, 0.18); mob.controller = controller; controller.moveTo(new Pos(5.5, 40, 0.5)); @@ -984,7 +995,7 @@ void temporaryDoorCapabilityPlansOpensAndTraversesDoor(Env env) { ControlledCreature zombie = spawn( instance, EntityType.ZOMBIE, new Pos(0.5, 40, 0.5)); EntityNavigationController controller = - EntityNavigationController.builtin(zombie, service, 0.18); + TestWorlds.builtinController(zombie, service, 0.18); zombie.controller = controller; controller.moveTo(new Pos(5.5, 40, 0.5), NavigationModifiers.builder() @@ -1012,7 +1023,7 @@ void pathIsRaisedWhenCauldronAppearsAfterPlanning(Env env) { ControlledCreature zombie = spawn( instance, EntityType.ZOMBIE, new Pos(0.5, 40, 0.5)); EntityNavigationController controller = - EntityNavigationController.builtin(zombie, service, 0.12); + TestWorlds.builtinController(zombie, service, 0.12); zombie.controller = controller; controller.moveTo(new Pos(5.5, 40, 0.5)); @@ -1062,7 +1073,7 @@ void spiderFallbackClimbsVerticalObstacle(Env env) { NavigationProfile profile = BuiltinNavigationProfiles .forEntityType(EntityType.SPIDER); EntityNavigationController controller = - new EntityNavigationController( + TestWorlds.controller( spider, service, profile, 0.15); spider.controller = controller; Pos goal = new Pos(4.5, 46, 0.5); @@ -1114,7 +1125,7 @@ void verticalTargetExpandsSearchAndUsesDistantStaircaseWithoutJumpingBelowIt( ControlledCreature mob = spawn( instance, EntityType.ZOMBIE, new Pos(0.5, 40, 0.5)); EntityNavigationController controller = - EntityNavigationController.builtin(mob, service, 0.16); + TestWorlds.builtinController(mob, service, 0.16); mob.controller = controller; NavigationRequest proofRequest = NavigationRequest.builder( instance, mob.getPosition(), new Pos(0.5, 48, 0.5), @@ -1180,7 +1191,7 @@ void optInLadderRouteEntersClimbsAndExitsUsingLivePhysics(Env env) { ControlledCreature mob = spawn(instance, EntityType.ZOMBIE, new Pos(0.5, 40, 0.5)); EntityNavigationController controller = - new EntityNavigationController(mob, service, climbing, 0.16); + TestWorlds.controller(mob, service, climbing, 0.16); mob.controller = controller; Pos climbGoal = new Pos(3.5, 45, 0.5); NavigationRequest meshRequest = new NavigationRequest( @@ -1209,13 +1220,16 @@ void optInLadderRouteEntersClimbsAndExitsUsingLivePhysics(Env env) { + mob.getPosition()); emit("opt-in-ladder-ascent", mob, controller); - GroundCapabilities descentCapabilities = GroundCapabilities.builder().maxStepHeight(1).maxFallDistance(1).allowDiagonal(true).horizontalEdgeCost(false).platformJump(PlatformJumpCapabilities.DISABLED).climbables(ClimbableCapabilities.STANDARD).build(); + GroundCapabilities descentCapabilities = GroundCapabilities.builder() + .maxFallDistance(1) + .climbables(ClimbableCapabilities.STANDARD) + .build(); NavigationProfile descending = base.withGroundCapabilities(descentCapabilities); ControlledCreature descendingMob = spawn( instance, EntityType.ZOMBIE, new Pos(2.5, 45, 0.5)); EntityNavigationController descendingController = - new EntityNavigationController( + TestWorlds.controller( descendingMob, service, descending, 0.16); descendingMob.controller = descendingController; descendingController.moveTo(new Pos(0.5, 40, 0.5)); @@ -1242,7 +1256,7 @@ void optInLadderRouteEntersClimbsAndExitsUsingLivePhysics(Env env) { new Pos(-1, 40, 0.5)); wideMob.setBoundingBox(1.8, 1.8, 0.8); EntityNavigationController wideController = - new EntityNavigationController( + TestWorlds.controller( wideMob, service, climbing, 0.16); wideMob.controller = wideController; wideController.moveTo(new Pos(3.5, 45, 0.5)); @@ -1278,15 +1292,16 @@ void everyNonLadderClimbableFamilyClimbsInLiveSimulation(Env env) { instance.setBlock(3, 44, 0, Block.STONE); NavigationProfile base = BuiltinNavigationProfiles.forEntityType( 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(); + NavigationProfile climbing = base.withGroundCapabilities( + base.groundCapabilities().withClimbables( + ClimbableCapabilities.STANDARD)); try (AsyncEntityPathfindingService service = new AsyncEntityPathfindingService(1, 8)) { ControlledCreature mob = spawn(instance, EntityType.ZOMBIE, new Pos(0.5, 40, 0.5)); EntityNavigationController controller = - new EntityNavigationController( + TestWorlds.controller( mob, service, climbing, 0.16); mob.controller = controller; controller.moveTo(new Pos(3.5, 45, 0.5)); @@ -1320,7 +1335,7 @@ void sunAvoidingMobStopsBeforeLeavingRoof(Env env) { ControlledCreature mob = spawn( instance, EntityType.ZOMBIE, new Pos(0.5, 40, 0.5)); EntityNavigationController controller = - EntityNavigationController.builtin(mob, service, 0.18); + TestWorlds.builtinController(mob, service, 0.18); mob.controller = controller; controller.moveTo(new Pos(8.5, 40, 0.5), NavigationModifiers.builder().avoidSun(true).build()); @@ -1340,9 +1355,7 @@ void sunAvoidingMobStopsBeforeLeavingRoof(Env env) { } private static Instance preparedFlat(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 3, - (x, z) -> instance.loadChunk(x, z).join()); + Instance instance = TestWorlds.loadedFlat(env, 3); return instance; } @@ -1357,7 +1370,10 @@ private static NavigationProfile jumpProfile(double distance) { BuiltinNavigationProfiles.forEntityType(EntityType.ZOMBIE); return base.withGroundCapabilities( base.groundCapabilities().withPlatformJump( - PlatformJumpCapabilities.builder().maxHorizontalDistance(distance).maxRise(0).maxDrop(0).apexClearance(1).build())); + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(distance) + .apexClearance(1) + .build())); } private static void runUntilFinished(Env env, EntityNavigationController controller, diff --git a/src/test/java/ca/atlasengine/pathfinding/MobProfileRoutingTest.java b/src/test/java/ca/atlasengine/pathfinding/MobProfileRoutingTest.java index 6f75625..2da8078 100644 --- a/src/test/java/ca/atlasengine/pathfinding/MobProfileRoutingTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/MobProfileRoutingTest.java @@ -1,6 +1,9 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.GroundCapabilities;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.search.DiscreteGroundPathfinder;import ca.atlasengine.pathfinding.search.GroundSearchLimits;import ca.atlasengine.pathfinding.search.SearchControl;import ca.atlasengine.pathfinding.terrain.MobTraversalProfile; +import ca.atlasengine.pathfinding.profile.GroundCapabilities; +import ca.atlasengine.pathfinding.result.PathResult; +import ca.atlasengine.pathfinding.search.SearchControl; +import ca.atlasengine.pathfinding.terrain.MobTraversalProfile; import ca.atlasengine.pathfinding.terrain.TerrainType; import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Pos; @@ -16,8 +19,6 @@ */ class MobProfileRoutingTest { private static final BoundingBox MOB = new BoundingBox(0.6, 1.8, 0.6); - private final DiscreteGroundPathfinder pathfinder = - new DiscreteGroundPathfinder(); /** A fire cell is forbidden to an animal and merely expensive to a warden. */ @Test @@ -107,8 +108,12 @@ void forbiddenWaterBlocksEntryFromDryLandButNotFromInsideIt() { assertEquals(-1, MobTraversalProfile.ENDERMAN.malus(TerrainType.WATER)); assertTrue(MobTraversalProfile.ENDERMAN.canFloat()); - PathResult fromDryLand = pathfinder.findPath(floodedLane(), new Pos(0.5, 1, 0.5), new Pos(5.5, 1, 0.5), MOB, MobTraversalProfile.ENDERMAN, GroundSearchLimits.builder().maxPathLength(20).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.STANDARD, SearchControl.NONE); - PathResult fromInsideWater = pathfinder.findPath(floodedLane(), new Pos(1.5, 1, 0.5), new Pos(5.5, 1, 0.5), MOB, MobTraversalProfile.ENDERMAN, GroundSearchLimits.builder().maxPathLength(20).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.STANDARD, SearchControl.NONE); + PathResult fromDryLand = GroundSearches.find(floodedLane(), new Pos(0.5, 1, 0.5), + new Pos(5.5, 1, 0.5), MOB, MobTraversalProfile.ENDERMAN, 20, 8, + GroundCapabilities.STANDARD, SearchControl.NONE); + PathResult fromInsideWater = GroundSearches.find(floodedLane(), new Pos(1.5, 1, 0.5), + new Pos(5.5, 1, 0.5), MOB, MobTraversalProfile.ENDERMAN, 20, 8, + GroundCapabilities.STANDARD, SearchControl.NONE); assertTrue(fromDryLand.found(), fromDryLand::toString); assertTrue(fromDryLand.nodes().stream().noneMatch(node -> @@ -129,7 +134,8 @@ void forbiddenWaterBlocksEntryFromDryLandButNotFromInsideIt() { } private PathResult lane(TestWorld world, MobTraversalProfile profile) { - return pathfinder.findPath(world, new Pos(0.5, 1, 0.5), new Pos(5.5, 1, 0.5), MOB, profile, GroundSearchLimits.builder().maxPathLength(20).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.STANDARD, SearchControl.NONE); + return GroundSearches.find(world, new Pos(0.5, 1, 0.5), new Pos(5.5, 1, 0.5), MOB, profile, + 20, 8, GroundCapabilities.STANDARD, SearchControl.NONE); } private static TestWorld lane() { diff --git a/src/test/java/ca/atlasengine/pathfinding/ModularControllerE2ETest.java b/src/test/java/ca/atlasengine/pathfinding/ModularControllerE2ETest.java index d37d027..83887f8 100644 --- a/src/test/java/ca/atlasengine/pathfinding/ModularControllerE2ETest.java +++ b/src/test/java/ca/atlasengine/pathfinding/ModularControllerE2ETest.java @@ -1,6 +1,10 @@ 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.search.EntityTraversalState;import ca.atlasengine.pathfinding.search.NavigationRequest;import net.minestom.server.coordinate.ChunkRange; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.NavigationProfile; +import ca.atlasengine.pathfinding.result.PathNode; +import ca.atlasengine.pathfinding.search.EntityTraversalState; +import ca.atlasengine.pathfinding.search.NavigationRequest; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.EntityCreature; import net.minestom.server.entity.EntityType; @@ -13,11 +17,12 @@ import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import static org.junit.jupiter.api.Assertions.assertTrue; @EnvTest class ModularControllerE2ETest { @@ -46,7 +51,7 @@ void swimmerUsesElevatedApertureWithoutClippingSolidVolume(Env env) { instance, EntityType.COD, new Pos(0.5, 40.5, 0.5)); EntityNavigationController controller = - EntityNavigationController.builtin(cod, service, 0.18); + TestWorlds.builtinController(cod, service, 0.18); cod.controller = controller; controller.moveTo(new Pos(6.5, 40.5, 0.5)); @@ -92,7 +97,7 @@ void waterControllerDispatchesAndFollowsThreeDimensionalSwimPath(Env env) { ControlledCreature cod = spawn( instance, EntityType.COD, new Pos(0.5, 40.5, 0.5)); EntityNavigationController controller = - EntityNavigationController.builtin(cod, service, 0.2); + TestWorlds.builtinController(cod, service, 0.2); cod.controller = controller; Pos goal = new Pos(6.5, 43.5, 0.5); NavigationProfile profile = BuiltinNavigationProfiles @@ -101,7 +106,7 @@ void waterControllerDispatchesAndFollowsThreeDimensionalSwimPath(Env env) { instance, cod.getPosition(), goal, cod.getBoundingBox(), profile, 16, 1, 1, List.of(), 63, new EntityTraversalState( - false, true, java.util.Set.of(), -64, 319, + false, true, Set.of(), -64, 319, List.of()), 16); controller.follow(AdaptiveMeshTestSupport.promoteAndReplay( @@ -142,7 +147,7 @@ void flyingControllerDispatchesAndFollowsPathAroundSolidVolume(Env env) { ControlledCreature bee = spawn( instance, EntityType.BEE, new Pos(0.5, 43.5, 0.5)); EntityNavigationController controller = - EntityNavigationController.builtin(bee, service, 0.2); + TestWorlds.builtinController(bee, service, 0.2); bee.controller = controller; Pos goal = new Pos(6.5, 43.5, 0.5); NavigationProfile profile = BuiltinNavigationProfiles @@ -188,7 +193,7 @@ void smallFlyerRecoversFromForbiddenNominalStartBeforeAsyncSearch(Env env) { ControlledCreature bee = spawn(instance, EntityType.BEE, new Pos(0.5, 43.5, 0.5), new UUID(0, 1)); EntityNavigationController controller = - EntityNavigationController.builtin(bee, service, 0.2); + TestWorlds.builtinController(bee, service, 0.2); bee.controller = controller; controller.moveTo(new Pos(4.5, 43.5, 0.5)); @@ -227,7 +232,7 @@ void amphibiousControllerCrossesPondAndReturnsToLand(Env env) { ControlledCreature axolotl = spawn( instance, EntityType.AXOLOTL, new Pos(0.5, 40, 0.5)); EntityNavigationController controller = - EntityNavigationController.builtin(axolotl, service, 0.16); + TestWorlds.builtinController(axolotl, service, 0.16); axolotl.controller = controller; Pos goal = new Pos(8.5, 40, 0.5); NavigationProfile profile = BuiltinNavigationProfiles @@ -264,9 +269,7 @@ void amphibiousControllerCrossesPondAndReturnsToLand(Env env) { } private static Instance prepared(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 3, - (x, z) -> instance.loadChunk(x, z).join()); + Instance instance = TestWorlds.loadedFlat(env, 3); return instance; } diff --git a/src/test/java/ca/atlasengine/pathfinding/MultiTargetPathfinderTest.java b/src/test/java/ca/atlasengine/pathfinding/MultiTargetPathfinderTest.java index 1bbf3f8..434dfd4 100644 --- a/src/test/java/ca/atlasengine/pathfinding/MultiTargetPathfinderTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/MultiTargetPathfinderTest.java @@ -1,6 +1,14 @@ 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.PathResult;import ca.atlasengine.pathfinding.result.PathStatus;import ca.atlasengine.pathfinding.search.EntityPathfinder;import ca.atlasengine.pathfinding.search.NavigationRequest;import ca.atlasengine.pathfinding.search.SearchControl;import net.minestom.server.collision.BoundingBox; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.NavigationProfile; +import ca.atlasengine.pathfinding.result.PathNode; +import ca.atlasengine.pathfinding.result.PathResult; +import ca.atlasengine.pathfinding.result.PathStatus; +import ca.atlasengine.pathfinding.search.EntityPathfinder; +import ca.atlasengine.pathfinding.search.NavigationRequest; +import ca.atlasengine.pathfinding.search.SearchControl; +import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.EntityType; diff --git a/src/test/java/ca/atlasengine/pathfinding/NavigationEventTest.java b/src/test/java/ca/atlasengine/pathfinding/NavigationEventTest.java index 337e269..6332039 100644 --- a/src/test/java/ca/atlasengine/pathfinding/NavigationEventTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/NavigationEventTest.java @@ -1,10 +1,11 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.result.PathStatus;import ca.atlasengine.pathfinding.event.NavigationCompletedEvent; +import ca.atlasengine.pathfinding.event.NavigationCancelledEvent; +import ca.atlasengine.pathfinding.event.NavigationCompletedEvent; import ca.atlasengine.pathfinding.event.NavigationEvent; import ca.atlasengine.pathfinding.event.PathComputedEvent; import ca.atlasengine.pathfinding.event.RouteReplanEvent; -import net.minestom.server.coordinate.ChunkRange; +import ca.atlasengine.pathfinding.result.PathStatus; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.EntityCreature; import net.minestom.server.entity.EntityType; @@ -34,11 +35,9 @@ class NavigationEventTest { @Test void eventsReachTheNodeOnTheTickingThread(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 1, - (x, z) -> instance.loadChunk(x, z).join()); - EntityCreature mob = new EntityCreature(EntityType.ZOMBIE); - mob.setInstance(instance, new Pos(0.5, 40, 0.5)).join(); + Instance instance = TestWorlds.loadedFlat(env, 1); + EntityCreature mob = TestWorlds.spawn(instance, EntityType.ZOMBIE, + new Pos(0.5, 40, 0.5)); env.tick(); List seen = new CopyOnWriteArrayList<>(); @@ -87,11 +86,9 @@ void eventsReachTheNodeOnTheTickingThread(Env env) { @Test void anOutcomeIsAnnouncedOnceRatherThanEveryTickItPersists(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 1, - (x, z) -> instance.loadChunk(x, z).join()); - EntityCreature mob = new EntityCreature(EntityType.ZOMBIE); - mob.setInstance(instance, new Pos(0.5, 40, 0.5)).join(); + Instance instance = TestWorlds.loadedFlat(env, 1); + EntityCreature mob = TestWorlds.spawn(instance, EntityType.ZOMBIE, + new Pos(0.5, 40, 0.5)); env.tick(); List arrivals = new CopyOnWriteArrayList<>(); @@ -102,14 +99,8 @@ void anOutcomeIsAnnouncedOnceRatherThanEveryTickItPersists(Env env) { EntityNavigationController controller = navigation.controller(mob); controller.moveTo(new Pos(3.5, 40, 0.5)); - long deadline = System.nanoTime() + Duration.ofSeconds(10).toNanos(); - while (arrivals.isEmpty()) { - if (System.nanoTime() > deadline) { - fail("never arrived: " + controller.state()); - } - controller.tick(); - env.tick(); - } + TestWorlds.tickUntil(env, controller, () -> !arrivals.isEmpty(), + () -> "never arrived: " + controller.state()); // COMPLETED persists, so a state-shaped implementation would // announce it again on every one of these ticks. for (int i = 0; i < 20; i++) { @@ -126,11 +117,9 @@ void anOutcomeIsAnnouncedOnceRatherThanEveryTickItPersists(Env env) { @Test void watchedBlockChangesReachEveryControllerTheSystemOwns(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 1, - (x, z) -> instance.loadChunk(x, z).join()); - EntityCreature mob = new EntityCreature(EntityType.ZOMBIE); - mob.setInstance(instance, new Pos(0.5, 40, 0.5)).join(); + Instance instance = TestWorlds.loadedFlat(env, 1); + EntityCreature mob = TestWorlds.spawn(instance, EntityType.ZOMBIE, + new Pos(0.5, 40, 0.5)); env.tick(); try (NavigationSystem navigation = NavigationSystem.builder() @@ -139,25 +128,15 @@ void watchedBlockChangesReachEveryControllerTheSystemOwns(Env env) { EntityNavigationController controller = navigation.controller(mob); controller.moveTo(new Pos(12.5, 40, 0.5)); - long deadline = System.nanoTime() + Duration.ofSeconds(10).toNanos(); - while (controller.nodes().isEmpty()) { - if (System.nanoTime() > deadline) fail("no route was produced"); - controller.tick(); - env.tick(); - } + TestWorlds.awaitRoute(env, controller); int before = controller.generation(); for (int y = 40; y < 43; y++) instance.setBlock(6, y, 0, Block.STONE); env.tick(); - deadline = System.nanoTime() + Duration.ofSeconds(10).toNanos(); - while (controller.generation() == before) { - if (System.nanoTime() > deadline) { - fail("a watched block change did not replan the route"); - } - controller.tick(); - env.tick(); - } + TestWorlds.tickUntil(env, controller, + () -> controller.generation() != before, + () -> "a watched block change did not replan the route"); assertTrue(controller.generation() > before, "the route was replanned after the world changed"); controller.close(); @@ -168,11 +147,9 @@ void watchedBlockChangesReachEveryControllerTheSystemOwns(Env env) { @Test void aBlockChangeIsAnnouncedAsItsOwnReplanReason(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 1, - (x, z) -> instance.loadChunk(x, z).join()); - EntityCreature mob = new EntityCreature(EntityType.ZOMBIE); - mob.setInstance(instance, new Pos(0.5, 40, 0.5)).join(); + Instance instance = TestWorlds.loadedFlat(env, 1); + EntityCreature mob = TestWorlds.spawn(instance, EntityType.ZOMBIE, + new Pos(0.5, 40, 0.5)); env.tick(); List reasons = new ArrayList<>(); @@ -184,26 +161,16 @@ void aBlockChangeIsAnnouncedAsItsOwnReplanReason(Env env) { EntityNavigationController controller = navigation.controller(mob); controller.moveTo(new Pos(12.5, 40, 0.5)); - long deadline = System.nanoTime() + Duration.ofSeconds(10).toNanos(); - while (controller.nodes().isEmpty()) { - if (System.nanoTime() > deadline) fail("no route was produced"); - controller.tick(); - env.tick(); - } + TestWorlds.awaitRoute(env, controller); assertTrue(reasons.contains(RouteReplanEvent.Reason.NEW_TARGET), "the first plan came from a new target, saw " + reasons); for (int y = 40; y < 43; y++) instance.setBlock(6, y, 0, Block.STONE); env.tick(); - deadline = System.nanoTime() + Duration.ofSeconds(10).toNanos(); - while (!reasons.contains(RouteReplanEvent.Reason.BLOCK_CHANGE)) { - if (System.nanoTime() > deadline) { - fail("a block change was not announced; saw " + reasons); - } - controller.tick(); - env.tick(); - } + TestWorlds.tickUntil(env, controller, + () -> reasons.contains(RouteReplanEvent.Reason.BLOCK_CHANGE), + () -> "a block change was not announced; saw " + reasons); controller.close(); } finally { mob.remove(); @@ -212,11 +179,9 @@ void aBlockChangeIsAnnouncedAsItsOwnReplanReason(Env env) { @Test void aSystemNobodyListensToBuildsNoEvents(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 1, - (x, z) -> instance.loadChunk(x, z).join()); - EntityCreature mob = new EntityCreature(EntityType.ZOMBIE); - mob.setInstance(instance, new Pos(0.5, 40, 0.5)).join(); + Instance instance = TestWorlds.loadedFlat(env, 1); + EntityCreature mob = TestWorlds.spawn(instance, EntityType.ZOMBIE, + new Pos(0.5, 40, 0.5)); env.tick(); try (NavigationSystem navigation = NavigationSystem.builder() @@ -224,14 +189,9 @@ void aSystemNobodyListensToBuildsNoEvents(Env env) { // eventNode() is never called, so no node exists to emit into. EntityNavigationController controller = navigation.controller(mob); controller.moveTo(new Pos(3.5, 40, 0.5)); - long deadline = System.nanoTime() + Duration.ofSeconds(10).toNanos(); - while (controller.state() != NavigationState.COMPLETED) { - if (System.nanoTime() > deadline) { - fail("never arrived: " + controller.state()); - } - controller.tick(); - env.tick(); - } + TestWorlds.tickUntil(env, controller, + () -> controller.state() == NavigationState.COMPLETED, + () -> "never arrived: " + controller.state()); // Navigating without an event node must behave identically. assertEquals(NavigationState.COMPLETED, controller.state()); controller.close(); @@ -242,11 +202,9 @@ void aSystemNobodyListensToBuildsNoEvents(Env env) { @Test void aNodeAskedForLateStillReceivesEvents(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 1, - (x, z) -> instance.loadChunk(x, z).join()); - EntityCreature mob = new EntityCreature(EntityType.ZOMBIE); - mob.setInstance(instance, new Pos(0.5, 40, 0.5)).join(); + Instance instance = TestWorlds.loadedFlat(env, 1); + EntityCreature mob = TestWorlds.spawn(instance, EntityType.ZOMBIE, + new Pos(0.5, 40, 0.5)); env.tick(); List seen = new CopyOnWriteArrayList<>(); @@ -258,15 +216,63 @@ void aNodeAskedForLateStillReceivesEvents(Env env) { .addListener(NavigationCompletedEvent.class, seen::add); controller.moveTo(new Pos(3.5, 40, 0.5)); - long deadline = System.nanoTime() + Duration.ofSeconds(10).toNanos(); - while (seen.isEmpty()) { - if (System.nanoTime() > deadline) { - fail("a controller created before the node was ignored"); - } - controller.tick(); - env.tick(); - } + TestWorlds.tickUntil(env, controller, () -> !seen.isEmpty(), + () -> "a controller created before the node was ignored"); + controller.close(); + } finally { + mob.remove(); + } + } + + @Test + void closingAFinishedNavigationKeepsItsOutcome(Env env) { + Instance instance = TestWorlds.loadedFlat(env, 1); + EntityCreature mob = TestWorlds.spawn(instance, EntityType.ZOMBIE, + new Pos(0.5, 40, 0.5)); + env.tick(); + + List cancellations = new CopyOnWriteArrayList<>(); + try (NavigationSystem navigation = NavigationSystem.builder() + .parallelism(1).queueCapacity(8).build()) { + navigation.eventNode().addListener( + NavigationCancelledEvent.class, cancellations::add); + EntityNavigationController controller = navigation.controller(mob); + controller.moveTo(new Pos(3.5, 40, 0.5)); + TestWorlds.tickUntil(env, controller, + () -> controller.state() == NavigationState.COMPLETED, + () -> "never arrived: " + controller.state()); + + controller.close(); + assertEquals(NavigationState.COMPLETED, controller.state(), + "closing does not rewrite how the navigation ended"); + assertTrue(cancellations.isEmpty(), + "a completed navigation is not also cancelled"); + } finally { + mob.remove(); + } + } + + @Test + void closingANavigationInProgressCancelsItOnce(Env env) { + Instance instance = TestWorlds.loadedFlat(env, 1); + EntityCreature mob = TestWorlds.spawn(instance, EntityType.ZOMBIE, + new Pos(0.5, 40, 0.5)); + env.tick(); + + List cancellations = new CopyOnWriteArrayList<>(); + try (NavigationSystem navigation = NavigationSystem.builder() + .parallelism(1).queueCapacity(8).build()) { + navigation.eventNode().addListener( + NavigationCancelledEvent.class, cancellations::add); + EntityNavigationController controller = navigation.controller(mob); + controller.moveTo(new Pos(12.5, 40, 0.5)); + TestWorlds.awaitRoute(env, controller); + + controller.close(); controller.close(); + assertEquals(NavigationState.CANCELLED, controller.state()); + assertEquals(1, cancellations.size(), + "one navigation reports one outcome"); } finally { mob.remove(); } diff --git a/src/test/java/ca/atlasengine/pathfinding/NavigationInfluenceTest.java b/src/test/java/ca/atlasengine/pathfinding/NavigationInfluenceTest.java index df4739d..3c515e3 100644 --- a/src/test/java/ca/atlasengine/pathfinding/NavigationInfluenceTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/NavigationInfluenceTest.java @@ -1,13 +1,21 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.profile.NavigationProfile;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.search.CancellationToken;import ca.atlasengine.pathfinding.search.EntityPathfinder;import ca.atlasengine.pathfinding.search.NavigationRequest;import ca.atlasengine.pathfinding.search.SearchControl;import ca.atlasengine.pathfinding.influence.BlockAvoidanceInfluence; +import ca.atlasengine.pathfinding.influence.BlockAvoidanceInfluence; import ca.atlasengine.pathfinding.influence.InfluenceResult; import ca.atlasengine.pathfinding.influence.NavigationInfluence; import ca.atlasengine.pathfinding.influence.NavigationZoneInfluence; import ca.atlasengine.pathfinding.influence.ReturnRadiusInfluence; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.NavigationProfile; +import ca.atlasengine.pathfinding.result.PathResult; +import ca.atlasengine.pathfinding.search.CancellationToken; +import ca.atlasengine.pathfinding.search.EntityPathfinder; +import ca.atlasengine.pathfinding.search.NavigationRequest; +import ca.atlasengine.pathfinding.search.SearchControl; 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.junit.jupiter.api.Test; @@ -50,7 +58,7 @@ void entityDispatcherComposesDynamicInfluences() { NavigationRequest request = NavigationRequest.builder( world, new Pos(0.5, 1, 0.5), new Pos(10.5, 1, 0.5), BOX, BuiltinNavigationProfiles.forEntityType( - net.minestom.server.entity.EntityType.ZOMBIE)) + EntityType.ZOMBIE)) .maxPathLength(30) .maxVisitedMultiplier(8) .influences(List.of(zone)) @@ -73,7 +81,7 @@ void returnRadiusBlocksOutwardTravelButAllowsReturnFromOutside() { NavigationInfluence boundary = new ReturnRadiusInfluence(home, outside, 32); NavigationProfile zombie = BuiltinNavigationProfiles.forEntityType( - net.minestom.server.entity.EntityType.ZOMBIE); + EntityType.ZOMBIE); PathResult outward = new EntityPathfinder().findPath( NavigationRequest.builder( diff --git a/src/test/java/ca/atlasengine/pathfinding/NavigationLifecycleBoundaryE2ETest.java b/src/test/java/ca/atlasengine/pathfinding/NavigationLifecycleBoundaryE2ETest.java index e3117cf..31b1e0e 100644 --- a/src/test/java/ca/atlasengine/pathfinding/NavigationLifecycleBoundaryE2ETest.java +++ b/src/test/java/ca/atlasengine/pathfinding/NavigationLifecycleBoundaryE2ETest.java @@ -1,22 +1,22 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.event.RouteReplanEvent; import ca.atlasengine.pathfinding.adaptive.SharedMeshOptions; import ca.atlasengine.pathfinding.adaptive.SharedMeshPursuit; +import ca.atlasengine.pathfinding.event.RouteReplanEvent; import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; import ca.atlasengine.pathfinding.result.PathNode; import ca.atlasengine.pathfinding.result.PathStatus; +import net.kyori.adventure.key.Key; import net.minestom.server.coordinate.ChunkRange; import net.minestom.server.coordinate.Pos; -import net.minestom.server.entity.EntityCreature; import net.minestom.server.entity.Entity; +import net.minestom.server.entity.EntityCreature; import net.minestom.server.entity.EntityType; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; import net.minestom.server.world.DimensionType; import net.minestom.testing.Env; import net.minestom.testing.EnvTest; -import net.kyori.adventure.key.Key; import org.junit.jupiter.api.Test; import java.time.Duration; @@ -41,9 +41,7 @@ class NavigationLifecycleBoundaryE2ETest { @Test void groundNavigationCrossesFromNegativeToPositiveChunks(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 2, - (x, z) -> instance.loadChunk(x, z).join()); + Instance instance = TestWorlds.loadedFlat(env, 2); EntityCreature mob = spawn(instance, new Pos(-6.5, 40, -0.5)); try (var service = new AsyncEntityPathfindingService(1, 8)) { @@ -64,9 +62,7 @@ void groundNavigationCrossesFromNegativeToPositiveChunks(Env env) { @Test void staleRouteBlockedByWallGetsOneSearchAndRecovers(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 2, - (x, z) -> instance.loadChunk(x, z).join()); + Instance instance = TestWorlds.loadedFlat(env, 2); EntityCreature mob = spawn(instance, new Pos(0.5, 40, 0.5)); for (int y = 40; y <= 42; y++) { instance.setBlock(4, y, 0, Block.STONE); @@ -82,12 +78,12 @@ void staleRouteBlockedByWallGetsOneSearchAndRecovers(Env env) { List staleStraightRoute = new ArrayList<>(); for (int x = 0; x <= 8; x++) { staleStraightRoute.add(new PathNode(x + 0.5, 40, 0.5, - PathNode.Movement.WALK)); + PathNode.Movement.WALK, x, 40, 0)); } controller.follow(new NavigationPlan(mob.getPosition(), target, mob.getBoundingBox(), BuiltinNavigationProfiles.forEntity(mob), PathStatus.FOUND, - staleStraightRoute, staleStraightRoute.size(), 0)); + staleStraightRoute, staleStraightRoute.size(), 0, List.of())); runUntilTerminal(env, controller, 900); @@ -231,7 +227,7 @@ void spatialPursuitsCloseWhenTargetLeavesTheirLifecycle(Env env) { private static EntityNavigationController controller( EntityCreature mob, AsyncEntityPathfindingService service) { - return new EntityNavigationController(mob, service, + return TestWorlds.controller(mob, service, BuiltinNavigationProfiles.forEntity(mob), SPEED); } @@ -242,9 +238,7 @@ private static EntityCreature spawn(Instance instance, Pos position) { } private static Instance preparedFlat(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 2, - (x, z) -> instance.loadChunk(x, z).join()); + Instance instance = TestWorlds.loadedFlat(env, 2); return instance; } diff --git a/src/test/java/ca/atlasengine/pathfinding/NavigationModifiersTest.java b/src/test/java/ca/atlasengine/pathfinding/NavigationModifiersTest.java index 2d43f2e..1657ff9 100644 --- a/src/test/java/ca/atlasengine/pathfinding/NavigationModifiersTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/NavigationModifiersTest.java @@ -1,6 +1,16 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.profile.ClimbableCapabilities;import ca.atlasengine.pathfinding.profile.NavigationModifiers;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.TerrainType; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.ClimbableCapabilities; +import ca.atlasengine.pathfinding.profile.NavigationModifiers; +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.TerrainType; import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.EntityType; @@ -144,7 +154,12 @@ void concurrentModifiedAndOrdinaryRequestsRemainIsolated() { @Test void modifierCopiesAndValidatesInput() { PlatformJumpCapabilities jump = - PlatformJumpCapabilities.builder().maxHorizontalDistance(3).maxRise(1).maxDrop(2).apexClearance(1).build(); + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(3) + .maxRise(1) + .maxDrop(2) + .apexClearance(1) + .build(); NavigationModifiers modifier = NavigationModifiers.builder() .terrainCost(TerrainType.LAVA, 3) .canFloat(true) diff --git a/src/test/java/ca/atlasengine/pathfinding/NavigationPlanCacheTest.java b/src/test/java/ca/atlasengine/pathfinding/NavigationPlanCacheTest.java index adb3d25..180bb9b 100644 --- a/src/test/java/ca/atlasengine/pathfinding/NavigationPlanCacheTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/NavigationPlanCacheTest.java @@ -1,13 +1,22 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.result.PathNode;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.result.PathStatus;import ca.atlasengine.pathfinding.search.EntityPathfinder;import ca.atlasengine.pathfinding.search.NavigationRequest;import ca.atlasengine.pathfinding.search.SearchControl;import ca.atlasengine.pathfinding.NavigationPlan; +import ca.atlasengine.pathfinding.NavigationPlan; import ca.atlasengine.pathfinding.NavigationPlanCache; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.result.PathNode; +import ca.atlasengine.pathfinding.result.PathResult; +import ca.atlasengine.pathfinding.result.PathStatus; +import ca.atlasengine.pathfinding.search.EntityPathfinder; +import ca.atlasengine.pathfinding.search.NavigationRequest; +import ca.atlasengine.pathfinding.search.SearchControl; import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.EntityType; import net.minestom.server.instance.block.Block; import org.junit.jupiter.api.Test; +import java.util.List; + import static org.junit.jupiter.api.Assertions.*; class NavigationPlanCacheTest { @@ -60,8 +69,8 @@ private static NavigationPlan simplePlan() { BuiltinNavigationProfiles.forEntityType(EntityType.ZOMBIE), PathStatus.FOUND, java.util.List.of( - new PathNode(0.5, 1, 0.5, PathNode.Movement.WALK), - new PathNode(1.5, 1, 0.5, PathNode.Movement.WALK)), - 2, 8); + new PathNode(0.5, 1, 0.5, PathNode.Movement.WALK, 0, 1, 0), + new PathNode(1.5, 1, 0.5, PathNode.Movement.WALK, 1, 1, 0)), + 2, 8, List.of()); } } diff --git a/src/test/java/ca/atlasengine/pathfinding/NavigationRequestTest.java b/src/test/java/ca/atlasengine/pathfinding/NavigationRequestTest.java index 29115fb..f9cae67 100644 --- a/src/test/java/ca/atlasengine/pathfinding/NavigationRequestTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/NavigationRequestTest.java @@ -1,7 +1,11 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.profile.NavigationProfile;import ca.atlasengine.pathfinding.search.EntityTraversalState;import ca.atlasengine.pathfinding.search.NavigationRequest;import ca.atlasengine.pathfinding.influence.NavigationInfluence; +import ca.atlasengine.pathfinding.influence.NavigationInfluence; import ca.atlasengine.pathfinding.influence.ReturnRadiusInfluence; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.NavigationProfile; +import ca.atlasengine.pathfinding.search.EntityTraversalState; +import ca.atlasengine.pathfinding.search.NavigationRequest; import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; diff --git a/src/test/java/ca/atlasengine/pathfinding/NavigationSystemTest.java b/src/test/java/ca/atlasengine/pathfinding/NavigationSystemTest.java index d61902c..0c42f87 100644 --- a/src/test/java/ca/atlasengine/pathfinding/NavigationSystemTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/NavigationSystemTest.java @@ -1,16 +1,23 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.metrics.NavigationMetricsSnapshot;import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.profile.NavigationProfile;import ca.atlasengine.pathfinding.result.PathNode;import ca.atlasengine.pathfinding.search.NavigationRequest;import ca.atlasengine.pathfinding.adaptive.SharedMeshPursuit; -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.internal.adaptive.NavigationPlanParity; +import ca.atlasengine.pathfinding.adaptive.SharedMeshHandle; 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.SharedMeshRequest; +import ca.atlasengine.pathfinding.adaptive.SharedMeshSource; import ca.atlasengine.pathfinding.adaptive.SharedMeshStatus; -import net.minestom.server.coordinate.ChunkRange; +import ca.atlasengine.pathfinding.event.NavigationEvent; +import ca.atlasengine.pathfinding.event.PathComputedEvent; +import ca.atlasengine.pathfinding.internal.adaptive.NavigationPlanParity; +import ca.atlasengine.pathfinding.metrics.NavigationMetricsSnapshot; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.NavigationMode; +import ca.atlasengine.pathfinding.profile.NavigationProfile; +import ca.atlasengine.pathfinding.result.PathNode; +import ca.atlasengine.pathfinding.search.NavigationRequest; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.EntityCreature; import net.minestom.server.entity.EntityType; @@ -20,6 +27,9 @@ import org.junit.jupiter.api.Test; import java.time.Duration; +import java.util.List; +import java.util.SplittableRandom; +import java.util.concurrent.CopyOnWriteArrayList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -30,11 +40,9 @@ class NavigationSystemTest { @Test void primaryFacadeNavigatesBuiltinEntityEndToEnd(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 1, - (x, z) -> instance.loadChunk(x, z).join()); - EntityCreature entity = new EntityCreature(EntityType.ZOMBIE); - entity.setInstance(instance, new Pos(0.5, 40, 0.5)).join(); + Instance instance = TestWorlds.loadedFlat(env, 1); + EntityCreature entity = TestWorlds.spawn(instance, EntityType.ZOMBIE, + new Pos(0.5, 40, 0.5)); try (NavigationSystem navigation = NavigationSystem.builder() .parallelism(1) @@ -110,11 +118,9 @@ void controllerSchedulingSettingsAreObservable() { @Test void computedPlanCanBeFedDirectlyToFollowerWithoutAnotherSearch(Env env) throws InterruptedException { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 1, - (x, z) -> instance.loadChunk(x, z).join()); - EntityCreature entity = new EntityCreature(EntityType.ZOMBIE); - entity.setInstance(instance, new Pos(0.5, 40, 0.5)).join(); + Instance instance = TestWorlds.loadedFlat(env, 1); + EntityCreature entity = TestWorlds.spawn(instance, EntityType.ZOMBIE, + new Pos(0.5, 40, 0.5)); NavigationProfile profile = BuiltinNavigationProfiles.forEntity(entity); try (NavigationSystem navigation = NavigationSystem.builder() @@ -153,9 +159,7 @@ void computedPlanCanBeFedDirectlyToFollowerWithoutAnotherSearch(Env env) @Test void adaptiveFacadePromotesRealAStarPlansAndReturnsFollowerReadyReuse(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 1, - (x, z) -> instance.loadChunk(x, z).join()); + Instance instance = TestWorlds.loadedFlat(env, 1); NavigationProfile profile = BuiltinNavigationProfiles.forEntityType(EntityType.ZOMBIE); NavigationRequest request = NavigationRequest.builder(instance, @@ -168,12 +172,29 @@ void adaptiveFacadePromotesRealAStarPlansAndReturnsFollowerReadyReuse(Env env) { try (NavigationSystem navigation = NavigationSystem.builder() .parallelism(1).queueCapacity(8) .sharedMesh(SharedMeshOptions.enabledWith(adaptive)).build()) { - SharedMeshHandle first = navigation.sharedMesh().plan(SharedMeshRequest.builder(request).actor("mob-1").target("player").worldRevision(1).currentTick(0).build()); - SharedMeshHandle second = navigation.sharedMesh().plan(SharedMeshRequest.builder(request).actor("mob-2").target("player").worldRevision(1).currentTick(1).build()); + SharedMeshHandle first = navigation.sharedMesh().plan(SharedMeshRequest.builder(request) + .actor("mob-1") + .target("player") + .worldRevision(1) + .currentTick(0) + .build()); + SharedMeshHandle second = navigation.sharedMesh().plan( + SharedMeshRequest.builder(request) + .actor("mob-2") + .target("player") + .worldRevision(1) + .currentTick(1) + .build()); assertTrue(first.plan().join().usable()); assertTrue(second.plan().join().usable()); - SharedMeshHandle shared = navigation.sharedMesh().plan(SharedMeshRequest.builder(request).actor("mob-3").target("player").worldRevision(1).currentTick(2).build()); + SharedMeshHandle shared = navigation.sharedMesh().plan( + SharedMeshRequest.builder(request) + .actor("mob-3") + .target("player") + .worldRevision(1) + .currentTick(2) + .build()); assertEquals(SharedMeshSource.SHARED_TARGET_FIELD, shared.source()); NavigationPlan replay = shared.plan().join(); @@ -190,9 +211,7 @@ void adaptiveFacadePromotesRealAStarPlansAndReturnsFollowerReadyReuse(Env env) { @Test void adaptiveThreeDimensionalRouteIsReusedByLiveFlyingFollower(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 2, - (x, z) -> instance.loadChunk(x, z).join()); + Instance instance = TestWorlds.loadedFlat(env, 2); EntityCreature bee = new EntityCreature(EntityType.BEE); Pos start = new Pos(0.5, 45, 0.5); Pos target = new Pos(12.5, 49, 5.5); @@ -207,11 +226,27 @@ void adaptiveThreeDimensionalRouteIsReusedByLiveFlyingFollower(Env env) { try (NavigationSystem navigation = NavigationSystem.builder() .parallelism(1).queueCapacity(8) .sharedMesh(SharedMeshOptions.enabledWith(adaptive)).build()) { - navigation.sharedMesh().plan(SharedMeshRequest.builder(request).actor("bee-seed-1").target("player").worldRevision(1).currentTick(0).build()) + navigation.sharedMesh().plan(SharedMeshRequest.builder(request) + .actor("bee-seed-1") + .target("player") + .worldRevision(1) + .currentTick(0) + .build()) .plan().join(); - navigation.sharedMesh().plan(SharedMeshRequest.builder(request).actor("bee-seed-2").target("player").worldRevision(1).currentTick(1).build()) + navigation.sharedMesh().plan(SharedMeshRequest.builder(request) + .actor("bee-seed-2") + .target("player") + .worldRevision(1) + .currentTick(1) + .build()) .plan().join(); - SharedMeshHandle shared = navigation.sharedMesh().plan(SharedMeshRequest.builder(request).actor(bee.getUuid()).target("player").worldRevision(1).currentTick(2).build()); + SharedMeshHandle shared = navigation.sharedMesh().plan( + SharedMeshRequest.builder(request) + .actor(bee.getUuid()) + .target("player") + .worldRevision(1) + .currentTick(2) + .build()); assertEquals(SharedMeshSource.SHARED_TARGET_FIELD, shared.source()); NavigationPlan plan = shared.plan().join(); @@ -241,9 +276,7 @@ void adaptiveThreeDimensionalRouteIsReusedByLiveFlyingFollower(Env env) { @Test void systemSnapshotCoversSearchesLatencyAndAdaptiveReuseAtOnce(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 1, - (x, z) -> instance.loadChunk(x, z).join()); + Instance instance = TestWorlds.loadedFlat(env, 1); NavigationProfile profile = BuiltinNavigationProfiles.forEntityType(EntityType.ZOMBIE); NavigationRequest request = NavigationRequest.builder(instance, @@ -259,11 +292,28 @@ void systemSnapshotCoversSearchesLatencyAndAdaptiveReuseAtOnce(Env env) { NavigationMetricsSnapshot before = navigation.metricsSnapshot(); assertEquals(0, before.searches().submitted()); - SharedMeshHandle first = navigation.sharedMesh().plan(SharedMeshRequest.builder(request).actor("mob-1").target("player").worldRevision(1).currentTick(0).build()); - SharedMeshHandle second = navigation.sharedMesh().plan(SharedMeshRequest.builder(request).actor("mob-2").target("player").worldRevision(1).currentTick(1).build()); + SharedMeshHandle first = navigation.sharedMesh().plan(SharedMeshRequest.builder(request) + .actor("mob-1") + .target("player") + .worldRevision(1) + .currentTick(0) + .build()); + SharedMeshHandle second = navigation.sharedMesh().plan( + SharedMeshRequest.builder(request) + .actor("mob-2") + .target("player") + .worldRevision(1) + .currentTick(1) + .build()); first.plan().join(); second.plan().join(); - SharedMeshHandle shared = navigation.sharedMesh().plan(SharedMeshRequest.builder(request).actor("mob-3").target("player").worldRevision(1).currentTick(2).build()); + SharedMeshHandle shared = navigation.sharedMesh().plan( + SharedMeshRequest.builder(request) + .actor("mob-3") + .target("player") + .worldRevision(1) + .currentTick(2) + .build()); shared.plan().join(); NavigationMetricsSnapshot after = navigation.metricsSnapshot(); @@ -303,11 +353,9 @@ void systemSnapshotCoversSearchesLatencyAndAdaptiveReuseAtOnce(Env env) { @Test void controllerOutcomesAreCountedWithoutRetainingAnyController(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 1, - (x, z) -> instance.loadChunk(x, z).join()); - EntityCreature entity = new EntityCreature(EntityType.ZOMBIE); - entity.setInstance(instance, new Pos(0.5, 40, 0.5)).join(); + Instance instance = TestWorlds.loadedFlat(env, 1); + EntityCreature entity = TestWorlds.spawn(instance, EntityType.ZOMBIE, + new Pos(0.5, 40, 0.5)); try (NavigationSystem navigation = NavigationSystem.builder() .parallelism(1).queueCapacity(8).movementPerTick(0.2).build()) { @@ -368,9 +416,7 @@ void controllerOutcomesAreCountedWithoutRetainingAnyController(Env env) { @Test void automaticEntityHandleFollowsMovingTargetAndCleansUpOnRemoval(Env env) throws InterruptedException { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 2, - (x, z) -> instance.loadChunk(x, z).join()); + Instance instance = TestWorlds.loadedFlat(env, 2); EntityCreature actor = new EntityCreature(EntityType.ZOMBIE); EntityCreature seedActorOne = new EntityCreature(EntityType.ZOMBIE); EntityCreature seedActorTwo = new EntityCreature(EntityType.ZOMBIE); @@ -621,9 +667,7 @@ void meshApiReachesTheSamePlanAsTheRawCoordinator(Env env) { } private static Instance flatInstance(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 1, - (x, z) -> instance.loadChunk(x, z).join()); + Instance instance = TestWorlds.loadedFlat(env, 1); return instance; } @@ -649,4 +693,76 @@ private static SharedMeshRequest meshRequest( .worldRevision(1).currentTick(tick) .strategy(strategy).build(); } + + @Test + void controllerBuilderAppliesEverySetting(Env env) { + Instance instance = TestWorlds.loadedFlat(env, 1); + EntityCreature mob = TestWorlds.spawn(instance, EntityType.ZOMBIE, + new Pos(0.5, 41, 0.5)); + mob.setNoGravity(true); + env.tick(); + + NavigationProfile flying = BuiltinNavigationProfiles + .forEntityType(EntityType.ZOMBIE) + .withNavigationType(NavigationMode.FLYING); + List computed = new CopyOnWriteArrayList<>(); + try (NavigationSystem navigation = NavigationSystem.builder() + .parallelism(1).queueCapacity(8).build()) { + navigation.eventNode().addListener( + PathComputedEvent.class, computed::add); + EntityNavigationController controller = navigation + .controllerBuilder(mob) + .profile(flying) + .movementPerTick(0.3) + .executionMode(MovementExecutionMode.DIRECT_COLLISION) + .startNodeRandom(new SplittableRandom(7)) + .build(); + controller.moveTo(new Pos(8.5, 43, 0.5)); + TestWorlds.awaitRoute(env, controller); + + assertTrue(controller.nodes().stream() + .allMatch(node -> node.movement() + == PathNode.Movement.FLY), + "the fixed profile planned the route: " + + controller.nodes()); + assertFalse(computed.isEmpty(), + "a built controller reports to the system's event node"); + Pos before = mob.getPosition(); + controller.tick(); + assertTrue(mob.getPosition().distance(before) > 0, + "direct execution moved the entity itself"); + assertTrue(mob.getPosition().distance(before) <= 0.3 + 1.0e-9, + "at the configured speed"); + assertEquals(0, mob.getVelocity().length(), 1.0e-9, + "direct execution leaves no velocity behind"); + controller.close(); + } finally { + mob.remove(); + } + } + + @Test + void controllerBuilderRejectsInvalidSettings(Env env) { + Instance instance = TestWorlds.loadedFlat(env, 0); + EntityCreature mob = TestWorlds.spawn(instance, EntityType.ZOMBIE, + new Pos(0.5, 40, 0.5)); + try (NavigationSystem navigation = NavigationSystem.create()) { + assertThrows(IllegalArgumentException.class, + () -> navigation.controllerBuilder(null)); + NavigationSystem.ControllerBuilder builder = + navigation.controllerBuilder(mob); + assertThrows(IllegalArgumentException.class, + () -> builder.profile(null)); + assertThrows(IllegalArgumentException.class, + () -> builder.movementPerTick(0)); + assertThrows(IllegalArgumentException.class, + () -> builder.movementPerTick(Double.NaN)); + assertThrows(IllegalArgumentException.class, + () -> builder.executionMode(null)); + assertThrows(IllegalArgumentException.class, + () -> builder.startNodeRandom(null)); + } finally { + mob.remove(); + } + } } diff --git a/src/test/java/ca/atlasengine/pathfinding/NavigatorEdgeCaseE2ETest.java b/src/test/java/ca/atlasengine/pathfinding/NavigatorEdgeCaseE2ETest.java index 13390c9..de1c29e 100644 --- a/src/test/java/ca/atlasengine/pathfinding/NavigatorEdgeCaseE2ETest.java +++ b/src/test/java/ca/atlasengine/pathfinding/NavigatorEdgeCaseE2ETest.java @@ -4,7 +4,6 @@ import ca.atlasengine.pathfinding.profile.NavigationProfile; import ca.atlasengine.pathfinding.search.EntityTraversalState; import ca.atlasengine.pathfinding.search.NavigationRequest; -import net.minestom.server.coordinate.ChunkRange; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.EntityCreature; import net.minestom.server.entity.EntityType; @@ -20,8 +19,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Black-box navigator checks using Minestom's real entity tick and collision @@ -186,8 +185,8 @@ private static void followReplay( private static EntityNavigationController controller( TrackedCreature creature, AsyncEntityPathfindingService service) { - EntityNavigationController controller = EntityNavigationController - .builtin(creature, service, 0.16); + EntityNavigationController controller = + TestWorlds.builtinController(creature, service, 0.16); creature.controller = controller; return controller; } @@ -231,9 +230,7 @@ private static void runUntilTerminal( } private static Instance preparedFlat(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 3, - (x, z) -> instance.loadChunk(x, z).join()); + Instance instance = TestWorlds.loadedFlat(env, 3); return instance; } diff --git a/src/test/java/ca/atlasengine/pathfinding/OpenableClearanceTest.java b/src/test/java/ca/atlasengine/pathfinding/OpenableClearanceTest.java index 2050d95..ce041f7 100644 --- a/src/test/java/ca/atlasengine/pathfinding/OpenableClearanceTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/OpenableClearanceTest.java @@ -1,6 +1,14 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.profile.ClimbableCapabilities;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.internal.movement.ClimbMovementExecutor; +import ca.atlasengine.pathfinding.internal.movement.ClimbMovementExecutor; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.ClimbableCapabilities; +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.BlockManipulationCapabilities; import ca.atlasengine.pathfinding.terrain.BlockTraversalData; import ca.atlasengine.pathfinding.terrain.MobTraversalProfile; diff --git a/src/test/java/ca/atlasengine/pathfinding/OpenableFootprintTest.java b/src/test/java/ca/atlasengine/pathfinding/OpenableFootprintTest.java index 040e757..7328080 100644 --- a/src/test/java/ca/atlasengine/pathfinding/OpenableFootprintTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/OpenableFootprintTest.java @@ -1,8 +1,18 @@ 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.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.internal.JumpArc; +import ca.atlasengine.pathfinding.internal.JumpArc; import ca.atlasengine.pathfinding.internal.movement.BlockManipulator; import ca.atlasengine.pathfinding.internal.movement.ClimbMovementExecutor; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.ClimbableCapabilities; +import ca.atlasengine.pathfinding.profile.GroundCapabilities; +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.BlockManipulationCapabilities; import ca.atlasengine.pathfinding.terrain.BlockTraversalData; import ca.atlasengine.pathfinding.terrain.MobTraversalProfile; @@ -263,7 +273,10 @@ private Planned jump(Block openable, BoundingBox box) { new Pos(center, 1, center), new Pos(far + center, 1, center), box, openable, GroundCapabilities.STANDARD.withPlatformJump( - PlatformJumpCapabilities.builder().maxHorizontalDistance(8).maxRise(0).maxDrop(0).apexClearance(APEX).build()))); + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(8) + .apexClearance(APEX) + .build()))); } // -------------------------------------------------------------- helpers diff --git a/src/test/java/ca/atlasengine/pathfinding/OpenableSwingClearanceTest.java b/src/test/java/ca/atlasengine/pathfinding/OpenableSwingClearanceTest.java index ac01971..fcfb3ea 100644 --- a/src/test/java/ca/atlasengine/pathfinding/OpenableSwingClearanceTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/OpenableSwingClearanceTest.java @@ -1,6 +1,12 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.profile.NavigationProfile;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.BlockManipulationCapabilities; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.NavigationProfile; +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.BlockManipulationCapabilities; import ca.atlasengine.pathfinding.terrain.MobTraversalProfile; import ca.atlasengine.pathfinding.terrain.OpenableBlockFamily; import ca.atlasengine.pathfinding.terrain.TerrainClassifier; @@ -32,7 +38,6 @@ class OpenableSwingClearanceTest { private static final String[] FACINGS = {"north", "south", "east", "west"}; private static final String[] HALVES = {"bottom", "top"}; private final EntityPathfinder pathfinding = new EntityPathfinder(); - private final TerrainClassifier classifier = new TerrainClassifier(); /** * The corridor runs along +x, so a panel that swings across x walls it @@ -243,7 +248,7 @@ private PathResult crossGap(TestWorld world, BoundingBox box, private TerrainType classify(TestWorld world, int x, int y, int z, BoundingBox box, MobTraversalProfile profile) { - return classifier.classifyAnchored(world, x, y, z, box, profile); + return TerrainClassifier.classifyAnchored(world, x, y, z, box, profile); } private static NavigationProfile navigation(MobTraversalProfile profile) { diff --git a/src/test/java/ca/atlasengine/pathfinding/OpenableSwingCorpusTest.java b/src/test/java/ca/atlasengine/pathfinding/OpenableSwingCorpusTest.java index fe632b8..cb2d34e 100644 --- a/src/test/java/ca/atlasengine/pathfinding/OpenableSwingCorpusTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/OpenableSwingCorpusTest.java @@ -1,6 +1,12 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;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.BuiltinNavigationProfiles; +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.entity.EntityType; @@ -190,6 +196,9 @@ private static TestWorld gapDoor() { "fence|w1.4|PARTIAL;0,1,0", "mixed|w0.6|FOUND;0,1,0;0,1,-1;1,1,-2;2,1,-3;3,1,-3;4,1,-3;5,1,-2;6,1,-2;6,1,-1;6,1,0", "mixed|w1.4|FOUND;-1,1,-1;-1,1,-2;0,1,-3;0,1,-4;1,1,-4;2,1,-4;3,1,-4;4,1,-4;5,1,-4;6,1,-3;6,1,-2;6,1,-1;6,1,0", + + + "wide_gap_trapdoor|w0.6|FOUND;0,1,0;1,1,0;2,1,0;3,1,0;4,1,0;5,1,0;6,1,0", "wide_gap_trapdoor|w1.4|FOUND;0,1,0;1,1,0;2,1,0;3,1,0;4,1,0;5,1,0;6,1,0", "wide_gap_door|w0.6|PARTIAL;0,1,0;1,1,0;2,1,0", @@ -226,6 +235,9 @@ private static TestWorld gapDoor() { "fence|w1.4|PARTIAL;0,1,0", "mixed|w0.6|FOUND;0,1,0;0,1,-1;1,1,-2;2,1,-3;3,1,-3;4,1,-3;5,1,-2;6,1,-2;6,1,-1;6,1,0", "mixed|w1.4|FOUND;-1,1,-1;-1,1,-2;0,1,-3;0,1,-4;1,1,-4;2,1,-4;3,1,-4;4,1,-4;5,1,-4;6,1,-3;6,1,-2;6,1,-1;6,1,0", + + + "wide_gap_trapdoor|w0.6|FOUND;0,1,0;1,1,0;2,1,0;3,1,0;4,1,0;5,1,0;6,1,0", "wide_gap_trapdoor|w1.4|FOUND;0,1,0;1,1,0;2,1,0;3,1,0;4,1,0;5,1,0;6,1,0", "wide_gap_door|w0.6|FOUND;0,1,0;1,1,0;2,1,0;3,1,0;4,1,0;5,1,0;6,1,0", diff --git a/src/test/java/ca/atlasengine/pathfinding/OpenableSwingE2ETest.java b/src/test/java/ca/atlasengine/pathfinding/OpenableSwingE2ETest.java index af91875..b01ee7c 100644 --- a/src/test/java/ca/atlasengine/pathfinding/OpenableSwingE2ETest.java +++ b/src/test/java/ca/atlasengine/pathfinding/OpenableSwingE2ETest.java @@ -1,9 +1,9 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.NavigationModifiers;import ca.atlasengine.pathfinding.terrain.BlockManipulationCapabilities; +import ca.atlasengine.pathfinding.profile.NavigationModifiers; +import ca.atlasengine.pathfinding.terrain.BlockManipulationCapabilities; import ca.atlasengine.pathfinding.terrain.OpenableBlockFamily; import net.minestom.server.collision.BoundingBox; -import net.minestom.server.coordinate.ChunkRange; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.EntityCreature; import net.minestom.server.entity.EntityType; @@ -124,7 +124,7 @@ private String drive(Env env, AsyncEntityPathfindingService service, mob.setBoundingBox(width, 1.95, width); mob.setInstance(instance, new Pos(0.5, SURFACE, 0.5)).join(); EntityNavigationController controller = - EntityNavigationController.builtin(mob, service, 0.18); + TestWorlds.builtinController(mob, service, 0.18); mob.controller = controller; controller.moveTo(new Pos(4.5, SURFACE, 0.5), NavigationModifiers.builder() @@ -181,9 +181,7 @@ private static Block trapdoor(String facing, String half) { /** Sealed one-wide corridor: a refused cell has no detour around it. */ private static Instance corridor(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 2, - (x, z) -> instance.loadChunk(x, z).join()); + Instance instance = TestWorlds.loadedFlat(env, 2); for (int x = -1; x <= 6; x++) { for (int y = SURFACE; y <= SURFACE + 2; y++) { instance.setBlock(x, y, -1, Block.STONE); diff --git a/src/test/java/ca/atlasengine/pathfinding/PartialChainRecoveryTest.java b/src/test/java/ca/atlasengine/pathfinding/PartialChainRecoveryTest.java index 58bbd10..1a1ac94 100644 --- a/src/test/java/ca/atlasengine/pathfinding/PartialChainRecoveryTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/PartialChainRecoveryTest.java @@ -1,6 +1,6 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import net.minestom.server.coordinate.ChunkRange; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.EntityCreature; import net.minestom.server.entity.EntityType; @@ -39,7 +39,7 @@ void aBudgetBoundPartialChainsOnToAReachableTargetInsideTheRadius(Env env) new AsyncEntityPathfindingService(1, 8)) { ControlledCreature mob = spawn(instance, new Pos(0.5, 40, 0.5)); EntityNavigationController controller = - new EntityNavigationController(mob, service, + TestWorlds.controller(mob, service, BuiltinNavigationProfiles.forEntityType( EntityType.ZOMBIE), 0.18); mob.controller = controller; @@ -87,9 +87,7 @@ void aBudgetBoundPartialChainsOnToAReachableTargetInsideTheRadius(Env env) * plain left on its frontier when the visit budget ends it. */ private static Instance barrier(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 3, - (x, z) -> instance.loadChunk(x, z).join()); + Instance instance = TestWorlds.loadedFlat(env, 3); for (int z = -30; z <= 30; z++) { if (z == GAP_Z) continue; for (int y = 40; y <= 42; y++) { diff --git a/src/test/java/ca/atlasengine/pathfinding/PartialRouteCutoffTest.java b/src/test/java/ca/atlasengine/pathfinding/PartialRouteCutoffTest.java index c329b20..7e929a6 100644 --- a/src/test/java/ca/atlasengine/pathfinding/PartialRouteCutoffTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/PartialRouteCutoffTest.java @@ -1,7 +1,9 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.result.PathNode;import ca.atlasengine.pathfinding.internal.movement.PartialRouteReplanner; +import ca.atlasengine.pathfinding.internal.movement.PartialRouteReplanner; import ca.atlasengine.pathfinding.internal.movement.PartialRouteReplanner.Continuation; +import ca.atlasengine.pathfinding.result.PathNode; +import ca.atlasengine.pathfinding.result.PathStop; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; import org.junit.jupiter.api.Test; @@ -31,12 +33,12 @@ void chainsOnlyWhatIsFurtherAwayThanTheTerminalRadius() { replanner.beginSegment(11); assertEquals(Continuation.REPLAN, replanner.continueLongPartialRoute( - ORIGIN, target(8.0001), route(3)), + ORIGIN, target(8.0001), route(3), PathStop.NOT_SEARCHED), "a target past the radius still deserves another segment"); replanner.beginSegment(11); assertEquals(Continuation.STOP, replanner.continueLongPartialRoute( - ORIGIN, target(8), route(4)), + ORIGIN, target(8), route(4), PathStop.NOT_SEARCHED), "a target inside the radius is terminal however productive " + "the segment that ended there was"); } @@ -48,11 +50,11 @@ void refusesToChainASegmentThatClosedLessThanTwoBlocks() { replanner.beginSegment(21.999); assertEquals(Continuation.STOP, replanner.continueLongPartialRoute( - ORIGIN, target(20), route(3))); + ORIGIN, target(20), route(3), PathStop.NOT_SEARCHED)); replanner.beginSegment(22); assertEquals(Continuation.REPLAN, replanner.continueLongPartialRoute( - ORIGIN, target(20), route(4))); + ORIGIN, target(20), route(4), PathStop.NOT_SEARCHED)); } @Test @@ -62,17 +64,17 @@ void neverSubmitsAnIdenticalPartialEndpointTwice() { replanner.beginSegment(22); assertEquals(Continuation.REPLAN, replanner.continueLongPartialRoute( - ORIGIN, target(20), route(5))); + ORIGIN, target(20), route(5), PathStop.NOT_SEARCHED)); replanner.beginSegment(22); assertEquals(Continuation.STOP, replanner.continueLongPartialRoute( - ORIGIN, target(20), route(5))); + ORIGIN, target(20), route(5), PathStop.NOT_SEARCHED)); // A new navigation forgets the endpoint the previous one exhausted. replanner.reset(40); replanner.beginSegment(22); assertEquals(Continuation.REPLAN, replanner.continueLongPartialRoute( - ORIGIN, target(20), route(5))); + ORIGIN, target(20), route(5), PathStop.NOT_SEARCHED)); } @Test @@ -82,9 +84,9 @@ void aRoutelessOrTargetlessFollowerIsTerminal() { replanner.beginSegment(22); assertEquals(Continuation.STOP, replanner.continueLongPartialRoute( - ORIGIN, null, route(5))); + ORIGIN, null, route(5), PathStop.NOT_SEARCHED)); assertEquals(Continuation.STOP, replanner.continueLongPartialRoute( - ORIGIN, target(20), List.of())); + ORIGIN, target(20), List.of(), PathStop.NOT_SEARCHED)); } @Test @@ -95,7 +97,7 @@ void aTargetHighAboveTheRadiusWidensUntilTheBudgetCeiling() { replanner.beginSegment(9); assertEquals(Continuation.WIDEN, replanner.continueLongPartialRoute( - ORIGIN, overhead, route(3)), + ORIGIN, overhead, route(3), PathStop.NOT_SEARCHED), "a rise the search could not cover is worth a wider budget"); assertEquals(1, replanner.searchRangeMultiplier()); @@ -109,7 +111,7 @@ ORIGIN, overhead, route(3)), replanner.beginSegment(9); assertEquals(Continuation.STOP, replanner.continueLongPartialRoute( - ORIGIN, overhead, route(4)), + ORIGIN, overhead, route(4), PathStop.NOT_SEARCHED), "an exhausted budget makes the rise terminal too"); } diff --git a/src/test/java/ca/atlasengine/pathfinding/PathOutputAuditTest.java b/src/test/java/ca/atlasengine/pathfinding/PathOutputAuditTest.java index 4c8aaf5..f691f98 100644 --- a/src/test/java/ca/atlasengine/pathfinding/PathOutputAuditTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/PathOutputAuditTest.java @@ -1,6 +1,13 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.profile.GroundCapabilities;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.GroundCapabilities; +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; @@ -22,7 +29,9 @@ class PathOutputAuditTest { @Test void emitRepresentativeRoutesAndVerifyTheirGeometry() { TestWorld flat = new TestWorld().floor(-15, 15, -15, 15, 0, Block.STONE); - PathResult straight = new DiscreteGroundPathfinder().findPath(flat, new Pos(0.5, 1, 0.5), new Pos(10.5, 1, 0.5), MOB, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(40).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.STANDARD, SearchControl.NONE); + PathResult straight = GroundSearches.find(flat, new Pos(0.5, 1, 0.5), new Pos(10.5, 1, 0.5), + MOB, MobTraversalProfile.DEFAULT, 40, 8, GroundCapabilities.STANDARD, + SearchControl.NONE); emit("straight", straight); assertEquals(11, straight.nodes().size()); for (int i = 0; i <= 10; i++) { @@ -33,7 +42,9 @@ void emitRepresentativeRoutesAndVerifyTheirGeometry() { TestWorld wall = new TestWorld().floor(-15, 15, -15, 15, 0, Block.STONE); for (int x = -2; x <= 2; x++) wall.column(x, 4, 1, 2, Block.STONE); - PathResult detour = new DiscreteGroundPathfinder().findPath(wall, new Pos(0.5, 1, 0.5), new Pos(0.5, 1, 8.5), MOB, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(40).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.STANDARD, SearchControl.NONE); + PathResult detour = GroundSearches.find(wall, new Pos(0.5, 1, 0.5), new Pos(0.5, 1, 8.5), + MOB, MobTraversalProfile.DEFAULT, 40, 8, GroundCapabilities.STANDARD, + SearchControl.NONE); emit("wall-detour", detour); assertTrue(detour.found()); assertEquals(8, detour.nodes().size() - 1, @@ -43,7 +54,9 @@ void emitRepresentativeRoutesAndVerifyTheirGeometry() { TestWorld hazard = new TestWorld().floor(-10, 10, -10, 10, 0, Block.STONE) .set(0, 1, 3, Block.CACTUS); - PathResult cactus = new DiscreteGroundPathfinder().findPath(hazard, new Pos(0.5, 1, 0.5), new Pos(0.5, 1, 7.5), MOB, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(40).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.STANDARD, SearchControl.NONE); + PathResult cactus = GroundSearches.find(hazard, new Pos(0.5, 1, 0.5), new Pos(0.5, 1, 7.5), + MOB, MobTraversalProfile.DEFAULT, 40, 8, GroundCapabilities.STANDARD, + SearchControl.NONE); emit("cactus-detour", cactus); assertTrue(cactus.nodes().stream().noneMatch(node -> node.blockX() == 0 && node.blockZ() == 3)); @@ -132,7 +145,8 @@ private static void emit(String label, PathResult result) { } private static PathResult groundPath(TestWorld world, MobTraversalProfile profile) { - return new DiscreteGroundPathfinder().findPath(world, new Pos(0.5, 1, 0.5), new Pos(5.5, 1, 0.5), MOB, profile, GroundSearchLimits.builder().maxPathLength(20).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.STANDARD, SearchControl.NONE); + return GroundSearches.find(world, new Pos(0.5, 1, 0.5), new Pos(5.5, 1, 0.5), MOB, profile, + 20, 8, GroundCapabilities.STANDARD, SearchControl.NONE); } private static double geometricLength(PathResult result) { diff --git a/src/test/java/ca/atlasengine/pathfinding/PathfindingProfileMain.java b/src/test/java/ca/atlasengine/pathfinding/PathfindingProfileMain.java index 50e518a..7577732 100644 --- a/src/test/java/ca/atlasengine/pathfinding/PathfindingProfileMain.java +++ b/src/test/java/ca/atlasengine/pathfinding/PathfindingProfileMain.java @@ -1,11 +1,21 @@ 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.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 jdk.jfr.Configuration; -import jdk.jfr.Recording; +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.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 jdk.jfr.Configuration; +import jdk.jfr.Recording; 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 java.lang.management.ManagementFactory; @@ -95,8 +105,9 @@ private static Workload groundWorkload() { if (z < -10 || z > -8) world.column(24, z, 1, 3, Block.STONE); if (z < 6 || z > 8) world.column(36, z, 1, 3, Block.STONE); } - DiscreteGroundPathfinder engine = new DiscreteGroundPathfinder(); - return () -> engine.findPath(world, new Pos(0.5, 1, 0.5), new Pos(45.5, 1, 0.5), new BoundingBox(0.8, 1.8, 0.8), MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(100).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.STANDARD, SearchControl.NONE); + return () -> GroundSearches.find(world, new Pos(0.5, 1, 0.5), new Pos(45.5, 1, 0.5), + new BoundingBox(0.8, 1.8, 0.8), MobTraversalProfile.DEFAULT, 100, 8, + GroundCapabilities.STANDARD, SearchControl.NONE); } private static Workload mazeWorkload() { @@ -109,8 +120,9 @@ private static Workload mazeWorkload() { } positive = !positive; } - DiscreteGroundPathfinder engine = new DiscreteGroundPathfinder(); - return () -> engine.findPath(world, new Pos(0.5, 1, -20.5), new Pos(0.5, 1, 20.5), new BoundingBox(0.8, 1.8, 0.8), MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(200).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.STANDARD, SearchControl.NONE); + return () -> GroundSearches.find(world, new Pos(0.5, 1, -20.5), new Pos(0.5, 1, 20.5), + new BoundingBox(0.8, 1.8, 0.8), MobTraversalProfile.DEFAULT, 200, 8, + GroundCapabilities.STANDARD, SearchControl.NONE); } private static Workload flyingWorkload() { @@ -121,7 +133,8 @@ private static Workload flyingWorkload() { } } EntityPathfinder engine = new EntityPathfinder(); - 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(); NavigationRequest request = NavigationRequest.builder(world, new Vec(0.5, 0.5, 0.5), new Vec(40.5, 0.5, 0.5), new BoundingBox(0.6, 0.6, 0.6), profile) @@ -138,7 +151,8 @@ private static Workload largeBoxWorkload() { } } EntityPathfinder engine = new EntityPathfinder(); - 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(); NavigationRequest request = NavigationRequest.builder(world, new Pos(0.5, 1, 0.5), new Pos(42.5, 1, 0.5), new BoundingBox(1.8, 2.7, 1.8), profile) @@ -157,9 +171,10 @@ private static Workload climbableWorkload() { } EntityPathfinder engine = new EntityPathfinder(); 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)); NavigationRequest request = NavigationRequest.builder(world, new Pos(0.5, 1, 0.5), new Pos(1.5, 33, 0.5), new BoundingBox(0.6, 1.8, 0.6), climbing) diff --git a/src/test/java/ca/atlasengine/pathfinding/PlanFollowToleranceTest.java b/src/test/java/ca/atlasengine/pathfinding/PlanFollowToleranceTest.java index 507e7ba..26ca032 100644 --- a/src/test/java/ca/atlasengine/pathfinding/PlanFollowToleranceTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/PlanFollowToleranceTest.java @@ -1,7 +1,9 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.result.PathNode;import ca.atlasengine.pathfinding.result.PathStatus;import ca.atlasengine.pathfinding.NavigationPlan; -import net.minestom.server.coordinate.ChunkRange; +import ca.atlasengine.pathfinding.NavigationPlan; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.result.PathNode; +import ca.atlasengine.pathfinding.result.PathStatus; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.Entity; import net.minestom.server.entity.EntityCreature; @@ -103,7 +105,7 @@ private static NavigationPlan driftedPlan(Entity entity, double drift) { NavigationPlan plan = new NavigationPlan(HOME, target, entity.getBoundingBox(), BuiltinNavigationProfiles.forEntity(entity), PathStatus.FOUND, - List.of(node(HOME), node(target)), 2, 8); + List.of(node(HOME), node(target)), 2, 8, List.of()); entity.teleport(HOME.withZ(HOME.z() + drift)).join(); assertEquals(drift, entity.getPosition().distance(plan.start()), 1.0e-9, "the fixture must place the entity exactly where it intends"); @@ -112,12 +114,13 @@ private static NavigationPlan driftedPlan(Entity entity, double drift) { private static PathNode node(Pos point) { return new PathNode(point.x(), point.y(), point.z(), - PathNode.Movement.WALK); + PathNode.Movement.WALK, + point.blockX(), point.blockY(), point.blockZ()); } private static EntityNavigationController controller( Entity entity, AsyncEntityPathfindingService service) { - return new EntityNavigationController(entity, service, + return TestWorlds.controller(entity, service, BuiltinNavigationProfiles.forEntity(entity), 0.18); } @@ -129,9 +132,7 @@ private static EntityCreature spawn( } private static Instance prepared(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 3, - (x, z) -> instance.loadChunk(x, z).join()); + Instance instance = TestWorlds.loadedFlat(env, 3); return instance; } } diff --git a/src/test/java/ca/atlasengine/pathfinding/PlatformJumpDoorE2ETest.java b/src/test/java/ca/atlasengine/pathfinding/PlatformJumpDoorE2ETest.java index e3e7d8f..5bc4ab5 100644 --- a/src/test/java/ca/atlasengine/pathfinding/PlatformJumpDoorE2ETest.java +++ b/src/test/java/ca/atlasengine/pathfinding/PlatformJumpDoorE2ETest.java @@ -1,12 +1,15 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.profile.NavigationProfile;import ca.atlasengine.pathfinding.profile.PlatformJumpCapabilities;import ca.atlasengine.pathfinding.result.PathNode;import ca.atlasengine.pathfinding.internal.movement.MovementContext; +import ca.atlasengine.pathfinding.internal.movement.MovementContext; import ca.atlasengine.pathfinding.internal.movement.PlatformJumpMovementExecutor; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.NavigationProfile; +import ca.atlasengine.pathfinding.profile.PlatformJumpCapabilities; +import ca.atlasengine.pathfinding.result.PathNode; import ca.atlasengine.pathfinding.terrain.BlockManipulationCapabilities; import ca.atlasengine.pathfinding.terrain.MobTraversalProfile; import ca.atlasengine.pathfinding.terrain.OpenableBlockFamily; import net.minestom.server.collision.BoundingBox; -import net.minestom.server.coordinate.ChunkRange; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.Entity; import net.minestom.server.entity.EntityCreature; @@ -47,7 +50,7 @@ void aMobThatOpensGatesJumpsOntoTheGatedPlatformAndLaunchesOnce(Env env) { ControlledCreature mob = spawn(instance, new Pos(0.5, SURFACE, 0.5)); EntityNavigationController controller = - new EntityNavigationController(mob, service, + TestWorlds.controller(mob, service, openingJumper(), 0.18); mob.controller = controller; controller.moveTo(new Pos(8.5, SURFACE, 0.5)); @@ -85,8 +88,8 @@ void theLaunchOpensItsOwnLandingBeforeItSweepsTheArc( boolean opens, Env env) { Instance instance = lane(env); instance.setBlock(4, SURFACE, 0, CLOSED_GATE); - EntityCreature mob = new EntityCreature(EntityType.ZOMBIE); - mob.setInstance(instance, new Pos(1.5, SURFACE, 0.5)).join(); + EntityCreature mob = TestWorlds.spawn(instance, EntityType.ZOMBIE, + new Pos(1.5, SURFACE, 0.5)); RecordingContext context = new RecordingContext(mob, opens, new PathNode(1.5, SURFACE, 0.5, PathNode.Movement.WALK, 1, SURFACE, 0), @@ -132,7 +135,7 @@ void aClosingProfileJumpsOntoADoorLandingWithoutClippingIt(Env env) { ControlledCreature mob = spawn(instance, new Pos(0.5, SURFACE, 0.5)); EntityNavigationController controller = - new EntityNavigationController(mob, service, + TestWorlds.controller(mob, service, closingJumper(), 0.18); mob.controller = controller; controller.moveTo(new Pos(8.5, SURFACE, 0.5)); @@ -185,9 +188,7 @@ private static NavigationProfile jumper( /** A one-cell lane walled in z, cut by a gap the mob has to jump. */ private static Instance lane(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 3, - (x, z) -> instance.loadChunk(x, z).join()); + Instance instance = TestWorlds.loadedFlat(env, 3); for (int y = SURFACE; y <= SURFACE + 3; y++) { for (int x = -3; x <= 13; x++) { instance.setBlock(x, y, -1, Block.STONE); diff --git a/src/test/java/ca/atlasengine/pathfinding/PlatformJumpDoorTest.java b/src/test/java/ca/atlasengine/pathfinding/PlatformJumpDoorTest.java index 8ce60c3..355c1a7 100644 --- a/src/test/java/ca/atlasengine/pathfinding/PlatformJumpDoorTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/PlatformJumpDoorTest.java @@ -1,6 +1,12 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.GroundCapabilities;import ca.atlasengine.pathfinding.profile.PlatformJumpCapabilities;import ca.atlasengine.pathfinding.result.PathNode;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.result.PathStatus;import ca.atlasengine.pathfinding.search.DiscreteGroundPathfinder;import ca.atlasengine.pathfinding.search.GroundSearchLimits;import ca.atlasengine.pathfinding.search.SearchControl;import ca.atlasengine.pathfinding.internal.JumpArc; +import ca.atlasengine.pathfinding.internal.JumpArc; +import ca.atlasengine.pathfinding.profile.GroundCapabilities; +import ca.atlasengine.pathfinding.profile.PlatformJumpCapabilities; +import ca.atlasengine.pathfinding.result.PathNode; +import ca.atlasengine.pathfinding.result.PathResult; +import ca.atlasengine.pathfinding.result.PathStatus; +import ca.atlasengine.pathfinding.search.SearchControl; import ca.atlasengine.pathfinding.terrain.BlockManipulationCapabilities; import ca.atlasengine.pathfinding.terrain.BlockTraversalData; import ca.atlasengine.pathfinding.terrain.MobTraversalProfile; @@ -101,11 +107,15 @@ void anArcOverAnOpenableBlockIsRefusedUntilThatBlockIsOpen(boolean open) { .set(2, 2, 0, CLOSED_GATE.withProperty( "open", String.valueOf(open))); PlatformJumpCapabilities jump = - PlatformJumpCapabilities.builder().maxHorizontalDistance(4).maxRise(0).maxDrop(0).apexClearance(1.5).build(); + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(4) + .apexClearance(1.5) + .build(); Pos start = new Pos(0.5, 1, 0.5); Pos goal = new Pos(4.5, 1, 0.5); - PathResult result = new DiscreteGroundPathfinder().findPath(world, start, goal, NARROW, OPENER, GroundSearchLimits.builder().maxPathLength(48).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.STANDARD.withPlatformJump(jump), SearchControl.NONE); + PathResult result = GroundSearches.find(world, start, goal, NARROW, OPENER, 48, 8, + GroundCapabilities.STANDARD.withPlatformJump(jump), SearchControl.NONE); assertEquals(open, result.found(), result.nodes()::toString); assertEquals(open, JumpArc.clear(world, NARROW, start, goal, 1.5, @@ -176,8 +186,14 @@ void aProfileThatOpensNothingKeepsOpenableBlocksOutOfEveryJump() { .set(3, 1, 0, CLOSED_GATE) .set(5, 0, 0, Block.STONE); - PathResult result = new DiscreteGroundPathfinder().findPath(world, new Pos(0.5, 1, 0.5), new Pos(5.5, 1, 0.5), NARROW, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(48).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.STANDARD.withPlatformJump( - PlatformJumpCapabilities.builder().maxHorizontalDistance(5).maxRise(0).maxDrop(0).apexClearance(1).build()), SearchControl.NONE); + PathResult result = GroundSearches.find(world, new Pos(0.5, 1, + 0.5), new Pos(5.5, 1, + 0.5), NARROW, MobTraversalProfile.DEFAULT, 48, 8, + GroundCapabilities.STANDARD.withPlatformJump( + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(5) + .apexClearance(1) + .build()), SearchControl.NONE); assertTrue(result.nodes().stream().noneMatch(node -> node.graphX() == 3), result.nodes()::toString); @@ -195,8 +211,17 @@ void aProfileThatOpensNothingKeepsOpenableBlocksOutOfEveryJump() { private List scanned(int span, int rise, double apex, int placement, Block obstacle, MobTraversalProfile profile) { - return new DiscreteGroundPathfinder().findPath(fixture(span, rise, placement, obstacle, profile), new Pos(0.5, 1, 0.5), new Pos(span + 0.5, rise + 1, 0.5), NARROW, profile, GroundSearchLimits.builder().maxPathLength(48).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.STANDARD.withPlatformJump( - PlatformJumpCapabilities.builder().maxHorizontalDistance(span).maxRise(Math.max(0, rise)).maxDrop(Math.max(0, -rise)).apexClearance(apex).build()), SearchControl.NONE) + return GroundSearches.find(fixture(span, rise, placement, obstacle, + profile), new Pos(0.5, 1, + 0.5), new Pos(span + 0.5, rise + 1, + 0.5), NARROW, profile, 48, 8, + GroundCapabilities.STANDARD.withPlatformJump( + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(span) + .maxRise(Math.max(0, rise)) + .maxDrop(Math.max(0, -rise)) + .apexClearance(apex) + .build()), SearchControl.NONE) .nodes(); } @@ -262,7 +287,12 @@ private static void assertLanded(PathResult result) { } private PathResult jump(TestWorld world, MobTraversalProfile profile) { - return new DiscreteGroundPathfinder().findPath(world, new Pos(0.5, 1, 0.5), new Pos(3.5, 1, 0.5), NARROW, profile, GroundSearchLimits.builder().maxPathLength(48).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.STANDARD.withPlatformJump( - PlatformJumpCapabilities.builder().maxHorizontalDistance(3).maxRise(0).maxDrop(0).apexClearance(1).build()), SearchControl.NONE); + return GroundSearches.find(world, new Pos(0.5, 1, + 0.5), new Pos(3.5, 1, + 0.5), NARROW, profile, 48, 8, GroundCapabilities.STANDARD.withPlatformJump( + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(3) + .apexClearance(1) + .build()), SearchControl.NONE); } } diff --git a/src/test/java/ca/atlasengine/pathfinding/PlatformJumpE2ETest.java b/src/test/java/ca/atlasengine/pathfinding/PlatformJumpE2ETest.java index 1315061..0711ee2 100644 --- a/src/test/java/ca/atlasengine/pathfinding/PlatformJumpE2ETest.java +++ b/src/test/java/ca/atlasengine/pathfinding/PlatformJumpE2ETest.java @@ -1,10 +1,13 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.profile.NavigationProfile;import ca.atlasengine.pathfinding.profile.PlatformJumpCapabilities;import ca.atlasengine.pathfinding.result.PathNode;import ca.atlasengine.pathfinding.internal.movement.MovementContext; +import ca.atlasengine.pathfinding.internal.movement.MovementContext; import ca.atlasengine.pathfinding.internal.movement.PlatformJumpMovementExecutor; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.NavigationProfile; +import ca.atlasengine.pathfinding.profile.PlatformJumpCapabilities; +import ca.atlasengine.pathfinding.result.PathNode; import net.minestom.server.collision.Aerodynamics; import net.minestom.server.collision.BoundingBox; -import net.minestom.server.coordinate.ChunkRange; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.Entity; import net.minestom.server.entity.EntityCreature; @@ -52,7 +55,7 @@ void plannedJumpsLaunchOnceForLevelRiseAndDropGeometries(Env env) { ControlledCreature mob = spawn(instance, EntityType.ZOMBIE, new Pos(0.5, SURFACE, z + 0.5)); EntityNavigationController controller = - new EntityNavigationController(mob, service, + TestWorlds.controller(mob, service, jumpProfile(EntityType.ZOMBIE, PlatformJumpCapabilities.acrossGaps(2) .withRise(1).withDrop(1)), @@ -113,9 +116,13 @@ void aCeilingOnlyTheRisingLaunchReachesIsNeverJumpedInto(Env env) { ControlledCreature mob = spawn(instance, EntityType.ZOMBIE, new Pos(0.5, SURFACE, 0.5)); EntityNavigationController controller = - new EntityNavigationController(mob, service, + TestWorlds.controller(mob, service, jumpProfile(EntityType.ZOMBIE, - PlatformJumpCapabilities.builder().maxHorizontalDistance(4).maxRise(2).maxDrop(0).apexClearance(1).build()), 0.18); + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(4) + .maxRise(2) + .apexClearance(1) + .build()), 0.18); mob.controller = controller; controller.moveTo(new Pos(8.5, SURFACE + 2, 0.5)); @@ -161,11 +168,14 @@ void wideFootprintMobCrossesAGapWithOneLaunchAndNoCollisions(Env env) { new Pos(1.0, SURFACE, 1.0)); mob.setBoundingBox(1.8, 1.8, 1.8); EntityNavigationController controller = - new EntityNavigationController(mob, service, + TestWorlds.controller(mob, service, jumpProfile(EntityType.RAVAGER, // Two straddled cells plus three gap // cells of anchor-to-anchor travel. - PlatformJumpCapabilities.builder().maxHorizontalDistance(5).maxRise(0).maxDrop(0).apexClearance(1).build()), + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(5) + .apexClearance(1) + .build()), 0.2); mob.controller = controller; controller.moveTo(new Pos(9.0, SURFACE, 1.0)); @@ -212,7 +222,7 @@ void anOffCentreApproachConvergesOnTheTakeoffAndStillCrosses(Env env) { ControlledCreature mob = spawn(instance, EntityType.ZOMBIE, new Pos(1.5, SURFACE, 0.94)); EntityNavigationController controller = - new EntityNavigationController(mob, service, + TestWorlds.controller(mob, service, jumpProfile(EntityType.ZOMBIE, PlatformJumpCapabilities.acrossGaps(2)), 0.18); @@ -295,8 +305,8 @@ void unreachableTakeoffReplansInsteadOfConvergingForever(Env env) { for (int y = SURFACE; y <= SURFACE + 2; y++) { instance.setBlock(1, y, 0, Block.STONE); } - EntityCreature mob = new EntityCreature(EntityType.ZOMBIE); - mob.setInstance(instance, new Pos(2.5, SURFACE, 0.5)).join(); + EntityCreature mob = TestWorlds.spawn(instance, EntityType.ZOMBIE, + new Pos(2.5, SURFACE, 0.5)); RecordingContext context = new RecordingContext(mob, new PathNode(2.1, SURFACE, 0.5, PathNode.Movement.WALK, 2, SURFACE, 0), @@ -339,9 +349,12 @@ void anArcTheFollowerCannotFlyIsNeverPlannedAndTheDetourIsTaken(Env env) { ControlledCreature mob = spawn(instance, EntityType.ZOMBIE, new Pos(-1.5, SURFACE, 0.5)); EntityNavigationController controller = - new EntityNavigationController(mob, service, + TestWorlds.controller(mob, service, jumpProfile(EntityType.ZOMBIE, - PlatformJumpCapabilities.builder().maxHorizontalDistance(5).maxRise(0).maxDrop(0).apexClearance(2.25).build()), 0.2); + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(5) + .apexClearance(2.25) + .build()), 0.2); mob.controller = controller; controller.moveTo(new Pos(5.5, SURFACE, 0.5)); @@ -393,9 +406,12 @@ void anArcTheEntitysOwnBallisticsCannotFlyNeverLaunches( standard.horizontalAirResistance(), standard.verticalAirResistance())); EntityNavigationController controller = - new EntityNavigationController(mob, service, + TestWorlds.controller(mob, service, jumpProfile(EntityType.ZOMBIE, - PlatformJumpCapabilities.builder().maxHorizontalDistance(3).maxRise(0).maxDrop(0).apexClearance(8).build()), 0.18); + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(3) + .apexClearance(8) + .build()), 0.18); mob.controller = controller; controller.moveTo(new Pos(8.5, SURFACE, 0.5)); @@ -431,7 +447,7 @@ void disabledCapabilityNeverLaunchesAcrossTheSameGap(Env env) { ControlledCreature mob = spawn(instance, EntityType.ZOMBIE, new Pos(0.5, SURFACE, 0.5)); EntityNavigationController controller = - new EntityNavigationController(mob, service, + TestWorlds.controller(mob, service, jumpProfile(EntityType.ZOMBIE, PlatformJumpCapabilities.DISABLED), 0.18); mob.controller = controller; @@ -519,9 +535,7 @@ private static NavigationProfile jumpProfile( } private static Instance preparedFlat(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 3, - (x, z) -> instance.loadChunk(x, z).join()); + Instance instance = TestWorlds.loadedFlat(env, 3); return instance; } diff --git a/src/test/java/ca/atlasengine/pathfinding/PlatformJumpTest.java b/src/test/java/ca/atlasengine/pathfinding/PlatformJumpTest.java index 6b5cefe..4d952f5 100644 --- a/src/test/java/ca/atlasengine/pathfinding/PlatformJumpTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/PlatformJumpTest.java @@ -1,6 +1,12 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.GroundCapabilities;import ca.atlasengine.pathfinding.profile.PlatformJumpCapabilities;import ca.atlasengine.pathfinding.result.PathNode;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.result.PathStatus;import ca.atlasengine.pathfinding.search.DiscreteGroundPathfinder;import ca.atlasengine.pathfinding.search.GroundSearchLimits;import ca.atlasengine.pathfinding.search.SearchControl;import ca.atlasengine.pathfinding.internal.JumpArc; +import ca.atlasengine.pathfinding.internal.JumpArc; +import ca.atlasengine.pathfinding.profile.GroundCapabilities; +import ca.atlasengine.pathfinding.profile.PlatformJumpCapabilities; +import ca.atlasengine.pathfinding.result.PathNode; +import ca.atlasengine.pathfinding.result.PathResult; +import ca.atlasengine.pathfinding.result.PathStatus; +import ca.atlasengine.pathfinding.search.SearchControl; import ca.atlasengine.pathfinding.terrain.MobTraversalProfile; import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Pos; @@ -22,14 +28,15 @@ class PlatformJumpTest { private static final BoundingBox NARROW = new BoundingBox(0.6, 1.8, 0.6); /** floor(1.8 + 1) = 2, so the footprint spans two cells per axis. */ private static final BoundingBox WIDE = new BoundingBox(1.8, 1.8, 1.8); - private final DiscreteGroundPathfinder pathfinder = - new DiscreteGroundPathfinder(); @ParameterizedTest @ValueSource(ints = {2, 3, 4, 5, 6}) void configuredDistanceCrossesExactlyThatManyCellsAndNoMore( int maximumDistance) { - PlatformJumpCapabilities jump = PlatformJumpCapabilities.builder().maxHorizontalDistance(maximumDistance).maxRise(0).maxDrop(0).apexClearance(1).build(); + PlatformJumpCapabilities jump = PlatformJumpCapabilities.builder() + .maxHorizontalDistance(maximumDistance) + .apexClearance(1) + .build(); PathResult exact = jumpPath(islands(maximumDistance), new Pos(0.5, 1, 0.5), @@ -53,7 +60,12 @@ void configuredDistanceCrossesExactlyThatManyCellsAndNoMore( @ValueSource(doubles = {0, 1, 1.5, 1.999}) void distancesBelowTwoAreDisabledAndEmitNoJumpEdges(double distance) { PlatformJumpCapabilities jump = - PlatformJumpCapabilities.builder().maxHorizontalDistance(distance).maxRise(4).maxDrop(4).apexClearance(1).build(); + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(distance) + .maxRise(4) + .maxDrop(4) + .apexClearance(1) + .build(); PathResult result = jumpPath(islands(2), new Pos(0.5, 1, 0.5), new Pos(2.5, 1, 0.5), NARROW, jump); @@ -82,7 +94,11 @@ void disabledConstantIsTheDefaultAndEmitsNoJumpEdges() { @ValueSource(ints = {0, 1, 2, 3}) void configuredRiseIsAnInclusiveBoundaryAtEveryLimit(int maximumRise) { PlatformJumpCapabilities jump = - PlatformJumpCapabilities.builder().maxHorizontalDistance(2).maxRise(maximumRise).maxDrop(0).apexClearance(2).build(); + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(2) + .maxRise(maximumRise) + .apexClearance(2) + .build(); PathResult exact = jumpPath(islandsAtY(2, maximumRise), new Pos(0.5, 1, 0.5), new Pos(2.5, maximumRise + 1, 0.5), @@ -102,7 +118,11 @@ void configuredRiseIsAnInclusiveBoundaryAtEveryLimit(int maximumRise) { @ValueSource(ints = {0, 1, 2, 3}) void configuredDropIsAnInclusiveBoundaryAtEveryLimit(int maximumDrop) { PlatformJumpCapabilities jump = - PlatformJumpCapabilities.builder().maxHorizontalDistance(2).maxRise(0).maxDrop(maximumDrop).apexClearance(2).build(); + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(2) + .maxDrop(maximumDrop) + .apexClearance(2) + .build(); PathResult exact = jumpPath(islandsAtY(2, -maximumDrop), new Pos(0.5, 1, 0.5), new Pos(2.5, 1 - maximumDrop, 0.5), @@ -127,9 +147,12 @@ void clearanceIsCheckedAlongTheArcRatherThanTheChord( Pos goal = new Pos(landingX + 0.5, 1, 0.5); PathResult chord = jumpPath(world, start, goal, NARROW, - PlatformJumpCapabilities.builder().maxHorizontalDistance(landingX).maxRise(0).maxDrop(0).apexClearance(0).build()); + PlatformJumpCapabilities.builder().maxHorizontalDistance(landingX).build()); PathResult arc = jumpPath(world, start, goal, NARROW, - PlatformJumpCapabilities.builder().maxHorizontalDistance(landingX).maxRise(0).maxDrop(0).apexClearance(1).build()); + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(landingX) + .apexClearance(1) + .build()); assertTrue(chord.found(), () -> "the straight chord is clear of the obstacle: " + chord); @@ -161,7 +184,11 @@ void aCeilingBetweenTheChordAndTheLaunchedApexIsRefused( TestWorld open = islandsAtY(2, rise); TestWorld roofed = islandsAtY(2, rise).set(ceilingX, ceilingY, 0, slab); PlatformJumpCapabilities jump = - PlatformJumpCapabilities.builder().maxHorizontalDistance(2).maxRise(rise).maxDrop(0).apexClearance(apex).build(); + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(2) + .maxRise(rise) + .apexClearance(apex) + .build(); Pos start = new Pos(0.5, 1, 0.5); Pos goal = new Pos(2.5, rise + 1, 0.5); @@ -188,7 +215,10 @@ void aCeilingBetweenTheChordAndTheLaunchedApexIsRefused( @Test void steepArcsAreSampledDenselyEnoughToSeeAnObstacle() { PlatformJumpCapabilities steep = - PlatformJumpCapabilities.builder().maxHorizontalDistance(2).maxRise(0).maxDrop(0).apexClearance(8).build(); + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(2) + .apexClearance(8) + .build(); assertTrue(jumpPath(islands(2), new Pos(0.5, 1, 0.5), new Pos(2.5, 1, 0.5), NARROW, steep).found()); @@ -203,13 +233,19 @@ void steepArcsAreSampledDenselyEnoughToSeeAnObstacle() { void wideFootprintJumpsBetweenFullySupportedAnchors() { TestWorld world = wideIslands(3, 3, 3); PlatformJumpCapabilities jump = - PlatformJumpCapabilities.builder().maxHorizontalDistance(5).maxRise(0).maxDrop(0).apexClearance(1).build(); + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(5) + .apexClearance(1) + .build(); PathResult jumped = jumpPath(world, new Pos(1.0, 1, 1.0), new Pos(7.0, 1, 1.0), WIDE, jump); PathResult tooShort = jumpPath(world, new Pos(1.0, 1, 1.0), new Pos(7.0, 1, 1.0), WIDE, - PlatformJumpCapabilities.builder().maxHorizontalDistance(4).maxRise(0).maxDrop(0).apexClearance(1).build()); + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(4) + .apexClearance(1) + .build()); PathNode landing = jumped.nodes().stream().filter(node -> node.movement() == PathNode.Movement.JUMP) @@ -233,7 +269,10 @@ void wideFootprintRejectsALandingNarrowerThanItself() { for (int x = 6; x < 9; x++) world.set(x, 0, 0, Block.AIR); for (int x = 6; x < 9; x++) world.set(x, 0, 2, Block.AIR); PlatformJumpCapabilities jump = - PlatformJumpCapabilities.builder().maxHorizontalDistance(6).maxRise(0).maxDrop(0).apexClearance(1).build(); + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(6) + .apexClearance(1) + .build(); PathResult result = jumpPath(world, new Pos(1.0, 1, 1.0), new Pos(7.0, 1, 1.0), WIDE, jump); @@ -255,7 +294,10 @@ void wideFootprintTakeoffMustBeFullySupported() { for (int x = 6; x < 9; x++) world.set(x, 0, z, Block.STONE); } PlatformJumpCapabilities jump = - PlatformJumpCapabilities.builder().maxHorizontalDistance(8).maxRise(0).maxDrop(0).apexClearance(1).build(); + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(8) + .apexClearance(1) + .build(); PathResult result = jumpPath(world, new Pos(0.5, 1, 1.0), new Pos(7.0, 1, 1.0), WIDE, jump); @@ -278,7 +320,12 @@ void onlyFullBlockSupportCanLaunchOrReceiveAJump( Block block = Block.fromState(support); assertNotNull(block, support); PlatformJumpCapabilities jump = - PlatformJumpCapabilities.builder().maxHorizontalDistance(2).maxRise(1).maxDrop(1).apexClearance(1).build(); + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(2) + .maxRise(1) + .maxDrop(1) + .apexClearance(1) + .build(); TestWorld takeoff = new TestWorld() .set(0, 0, 0, block).set(2, 0, 0, Block.STONE); TestWorld landing = new TestWorld() @@ -302,12 +349,22 @@ void continuousGroundNeverProducesJumpEdges() { PathResult result = jumpPath(world, new Pos(0.5, 1, 0.5), new Pos(8.5, 1, 0.5), NARROW, - PlatformJumpCapabilities.builder().maxHorizontalDistance(6).maxRise(2).maxDrop(2).apexClearance(1).build()); + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(6) + .maxRise(2) + .maxDrop(2) + .apexClearance(1) + .build()); PathResult wide = jumpPath( new TestWorld().floor(-2, 10, -2, 6, 0, Block.STONE), new Pos(1.0, 1, 1.0), new Pos(8.0, 1, 1.0), WIDE, - PlatformJumpCapabilities.builder().maxHorizontalDistance(6).maxRise(2).maxDrop(2).apexClearance(1).build()); + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(6) + .maxRise(2) + .maxDrop(2) + .apexClearance(1) + .build()); assertTrue(result.found(), result::toString); assertTrue(result.nodes().stream().noneMatch(node -> @@ -350,7 +407,12 @@ void everyEmittedJumpSurvivesTheSweptTestTheFollowerRuns( int obstacleX, int obstacleY) { TestWorld world = islandsAtY(landingX, rise) .set(obstacleX, obstacleY, 0, Block.STONE); - PlatformJumpCapabilities jump = PlatformJumpCapabilities.builder().maxHorizontalDistance(landingX).maxRise(Math.max(0, rise)).maxDrop(Math.max(0, -rise)).apexClearance(apex).build(); + PlatformJumpCapabilities jump = PlatformJumpCapabilities.builder() + .maxHorizontalDistance(landingX) + .maxRise(Math.max(0, rise)) + .maxDrop(Math.max(0, -rise)) + .apexClearance(apex) + .build(); Pos start = new Pos(0.5, 1, 0.5); Pos goal = new Pos(landingX + 0.5, rise + 1, 0.5); @@ -393,7 +455,10 @@ void aJumpClearsASupportedIslandTheMobMayNotStandOn( .set(2, 1, 0, block); if (island.equals("stone")) world.set(2, 2, 0, Block.STONE); PlatformJumpCapabilities jump = - PlatformJumpCapabilities.builder().maxHorizontalDistance(4).maxRise(0).maxDrop(0).apexClearance(apex).build(); + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(4) + .apexClearance(apex) + .build(); PathResult result = jumpPath(world, new Pos(0.5, 1, 0.5), new Pos(4.5, 1, 0.5), NARROW, jump); @@ -409,7 +474,11 @@ void aJumpClearsASupportedIslandTheMobMayNotStandOn( "the arc over the island must survive the swept test"); assertFalse(jumpPath(world, new Pos(0.5, 1, 0.5), new Pos(4.5, 1, 0.5), NARROW, - PlatformJumpCapabilities.builder().maxHorizontalDistance(4).maxRise(0).maxDrop(0).apexClearance(0.25).build()).found(), + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(4) + .apexClearance(0.25) + .build()) + .found(), "a flat arc must still be refused by the swept test"); } @@ -417,7 +486,10 @@ void aJumpClearsASupportedIslandTheMobMayNotStandOn( void aSupportedWalkableIslandIsLandedOnRatherThanFlownOver() { TestWorld world = islands(4).set(2, 0, 0, Block.STONE); PlatformJumpCapabilities jump = - PlatformJumpCapabilities.builder().maxHorizontalDistance(4).maxRise(0).maxDrop(0).apexClearance(1).build(); + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(4) + .apexClearance(1) + .build(); PathResult result = jumpPath(world, new Pos(0.5, 1, 0.5), new Pos(4.5, 1, 0.5), NARROW, jump); @@ -438,9 +510,14 @@ void anIslandImpassableForThisProfileIsJumpedOverAndNeverEnteredNothingElse() { TestWorld world = islands(4).set(2, 0, 0, Block.STONE) .set(2, 1, 0, Block.WATER); PlatformJumpCapabilities jump = - PlatformJumpCapabilities.builder().maxHorizontalDistance(4).maxRise(0).maxDrop(0).apexClearance(1).build(); - - PathResult avoidsWater = pathfinder.findPath(world, new Pos(0.5, 1, 0.5), new Pos(4.5, 1, 0.5), NARROW, MobTraversalProfile.ENDERMAN, GroundSearchLimits.builder().maxPathLength(48).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.STANDARD.withPlatformJump(jump), SearchControl.NONE); + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(4) + .apexClearance(1) + .build(); + + PathResult avoidsWater = GroundSearches.find(world, new Pos(0.5, 1, 0.5), + new Pos(4.5, 1, 0.5), NARROW, MobTraversalProfile.ENDERMAN, 48, 8, + GroundCapabilities.STANDARD.withPlatformJump(jump), SearchControl.NONE); PathResult wadesWater = jumpPath(world, new Pos(0.5, 1, 0.5), new Pos(4.5, 1, 0.5), NARROW, jump); @@ -460,13 +537,19 @@ void anIslandImpassableForThisProfileIsJumpedOverAndNeverEnteredNothingElse() { @Test void capabilityRecordRejectsOutOfRangeAndNonFiniteValues() { assertThrows(IllegalArgumentException.class, - () -> PlatformJumpCapabilities.builder().maxHorizontalDistance(-1).maxRise(0).maxDrop(0).apexClearance(0).build()); + () -> PlatformJumpCapabilities.builder().maxHorizontalDistance(-1).build()); assertThrows(IllegalArgumentException.class, - () -> PlatformJumpCapabilities.builder().maxHorizontalDistance(33).maxRise(0).maxDrop(0).apexClearance(0).build()); + () -> PlatformJumpCapabilities.builder().maxHorizontalDistance(33).build()); assertThrows(IllegalArgumentException.class, - () -> PlatformJumpCapabilities.builder().maxHorizontalDistance(2).maxRise(17).maxDrop(0).apexClearance(0).build()); + () -> PlatformJumpCapabilities.builder() + .maxHorizontalDistance(2) + .maxRise(17) + .build()); assertThrows(IllegalArgumentException.class, - () -> PlatformJumpCapabilities.builder().maxHorizontalDistance(2).maxRise(0).maxDrop(0).apexClearance(Double.NaN).build()); + () -> PlatformJumpCapabilities.builder() + .maxHorizontalDistance(2) + .apexClearance(Double.NaN) + .build()); assertThrows(IllegalArgumentException.class, () -> PlatformJumpCapabilities.acrossGaps(-1)); } @@ -499,7 +582,14 @@ void withersLayerRiseDropAndClearanceOntoAGapSizedBase() { PlatformJumpCapabilities jump = PlatformJumpCapabilities.acrossGaps(2) .withRise(1).withDrop(3).withApexClearance(2); - assertEquals(PlatformJumpCapabilities.builder().maxHorizontalDistance(3).maxRise(1).maxDrop(3).apexClearance(2).build(), jump); + assertEquals( + PlatformJumpCapabilities.builder() + .maxHorizontalDistance(3) + .maxRise(1) + .maxDrop(3) + .apexClearance(2) + .build(), + jump); assertTrue(jumpPath(islandsAtY(3, 1), new Pos(0.5, 1, 0.5), new Pos(3.5, 2, 0.5), NARROW, jump).found()); assertTrue(jumpPath(islandsAtY(3, -3), new Pos(0.5, 1, 0.5), @@ -511,7 +601,8 @@ void withersLayerRiseDropAndClearanceOntoAGapSizedBase() { private PathResult jumpPath(TestWorld world, Pos start, Pos goal, BoundingBox box, PlatformJumpCapabilities jump) { - return pathfinder.findPath(world, start, goal, box, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(48).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.STANDARD.withPlatformJump(jump), SearchControl.NONE); + return GroundSearches.find(world, start, goal, box, MobTraversalProfile.DEFAULT, 48, 8, + GroundCapabilities.STANDARD.withPlatformJump(jump), SearchControl.NONE); } private static TestWorld islands(int landingX) { diff --git a/src/test/java/ca/atlasengine/pathfinding/RouteFreshnessTest.java b/src/test/java/ca/atlasengine/pathfinding/RouteFreshnessTest.java index 4c94105..a56d516 100644 --- a/src/test/java/ca/atlasengine/pathfinding/RouteFreshnessTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/RouteFreshnessTest.java @@ -1,17 +1,19 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.metrics.NavigationMetricsSnapshot;import ca.atlasengine.pathfinding.result.PathNode;import ca.atlasengine.pathfinding.adaptive.SharedMeshPursuit; -import ca.atlasengine.pathfinding.internal.movement.RouteSplicer; +import ca.atlasengine.pathfinding.adaptive.SharedMeshPursuit; +import ca.atlasengine.pathfinding.metrics.NavigationMetricsSnapshot; +import ca.atlasengine.pathfinding.metrics.SpliceCounts; +import ca.atlasengine.pathfinding.result.PathNode; import net.minestom.server.collision.BoundingBox; import net.minestom.server.collision.CollisionUtils; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Entity; import net.minestom.server.entity.EntityCreature; import net.minestom.server.entity.EntityType; import net.minestom.server.instance.Instance; import net.minestom.server.instance.block.Block; -import net.minestom.server.coordinate.Vec; import net.minestom.testing.Env; import net.minestom.testing.EnvTest; import org.junit.jupiter.api.Test; @@ -123,8 +125,8 @@ void everySplicedRouteIsContiguousAndSweepable(Env env) { PursuitHarness.SearchGate gate = new PursuitHarness.SearchGate(); Instance instance = loadedGatedFlatInstance(env, gate); obstruct(instance); - EntityCreature actor = new EntityCreature(EntityType.ZOMBIE); - actor.setInstance(instance, new Pos(0.5, SURFACE_Y, 0.5)).join(); + EntityCreature actor = TestWorlds.spawn(instance, EntityType.ZOMBIE, + new Pos(0.5, SURFACE_Y, 0.5)); for (int settle = 0; settle < 3; settle++) env.tick(); NavigationSystem system = NavigationSystem.builder() @@ -157,7 +159,7 @@ void everySplicedRouteIsContiguousAndSweepable(Env env) { } env.tick(); } - RouteSplicer.Counts splices = controller.spliceCounts(); + SpliceCounts splices = controller.spliceCounts(); assertTrue(splices.joined() > 0, () -> "no route was spliced, so nothing was proved: " + splices); @@ -187,8 +189,8 @@ void everySplicedRouteIsContiguousAndSweepable(Env env) { void aReversingTargetRefusesSplicesAndStillKeepsRoutes(Env env) { PursuitHarness.SearchGate gate = new PursuitHarness.SearchGate(); Instance instance = loadedGatedFlatInstance(env, gate); - EntityCreature actor = new EntityCreature(EntityType.ZOMBIE); - actor.setInstance(instance, new Pos(0.5, SURFACE_Y, 0.5)).join(); + EntityCreature actor = TestWorlds.spawn(instance, EntityType.ZOMBIE, + new Pos(0.5, SURFACE_Y, 0.5)); for (int settle = 0; settle < 3; settle++) env.tick(); NavigationSystem system = NavigationSystem.builder() @@ -219,7 +221,7 @@ void aReversingTargetRefusesSplicesAndStillKeepsRoutes(Env env) { if (state == NavigationState.FOLLOWING || state == NavigationState.PARTIAL) ticksWithRoute++; } - RouteSplicer.Counts splices = controller.spliceCounts(); + SpliceCounts splices = controller.spliceCounts(); assertTrue(splices.divergent() > 0, () -> "a reversing target never refused a splice: " + splices); @@ -277,7 +279,7 @@ private static void assertSweepable( Vec start = previous.asVec().add(0, 1.0e-5, 0); Vec end = current.asVec().add(0, 1.0e-5, 0); var physics = CollisionUtils.handlePhysics(instance, box, - start.asPos(), end.sub(start).asVec(), null, false); + start.asPos(), end.sub(start), null, false); assertFalse(physics.collisionX() || physics.collisionY() || physics.collisionZ(), () -> "blocked segment " + previous + " -> " + current @@ -292,7 +294,7 @@ private static String report(String label, List sweep) { + "meanToTarget travelled sub/done " + "armed/joined seam/over/exp/sup short/div\n"); for (Freshness run : sweep) { - RouteSplicer.Counts splices = run.splices(); + SpliceCounts splices = run.splices(); table.append(String.format(Locale.ROOT, "%3dt/%4dms %6d %8.3f %8.3f %6d %13.3f %9.2f %5d/%-5d " + "%6d/%-6d %3d/%3d/%3d/%3d %3d/%3d%n", @@ -318,14 +320,14 @@ private record Freshness( int latencyTicks, int adoptions, double meanAdoptGap, double maxAdoptGap, long stalePlans, double meanTargetDistance, double travelled, long submitted, long completed, - RouteSplicer.Counts splices) { + SpliceCounts splices) { } private static Freshness pursueController(Env env, int latencyTicks) { PursuitHarness.SearchGate gate = new PursuitHarness.SearchGate(); Instance instance = loadedGatedFlatInstance(env, gate); - EntityCreature actor = new EntityCreature(EntityType.ZOMBIE); - actor.setInstance(instance, new Pos(0.5, SURFACE_Y, 0.5)).join(); + EntityCreature actor = TestWorlds.spawn(instance, EntityType.ZOMBIE, + new Pos(0.5, SURFACE_Y, 0.5)); for (int settle = 0; settle < 3; settle++) env.tick(); NavigationSystem system = NavigationSystem.builder() @@ -402,7 +404,7 @@ private static Freshness pursueAdaptive(Env env, int latencyTicks) { target.getPosition(), route, navigation.nodes()); env.tick(); } - RouteSplicer.Counts splices = navigation.spliceCounts(); + SpliceCounts splices = navigation.spliceCounts(); navigation.close(); return sampler.freshness(latencyTicks, system.metricsSnapshot(), splices, TICKS); @@ -446,7 +448,7 @@ private List sample(Point position, Point target, private Freshness freshness( int latencyTicks, NavigationMetricsSnapshot end, - RouteSplicer.Counts splices, int ticks) { + SpliceCounts splices, int ticks) { return new Freshness(latencyTicks, adoptions, adoptions == 0 ? 0 : totalGap / adoptions, maximumGap, end.stalePlans(), totalTargetDistance / ticks, travelled, diff --git a/src/test/java/ca/atlasengine/pathfinding/SearchCancellationTest.java b/src/test/java/ca/atlasengine/pathfinding/SearchCancellationTest.java index a4c7de7..83abd08 100644 --- a/src/test/java/ca/atlasengine/pathfinding/SearchCancellationTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/SearchCancellationTest.java @@ -1,6 +1,15 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.profile.GroundCapabilities;import ca.atlasengine.pathfinding.result.PathNode;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.result.PathStatus;import ca.atlasengine.pathfinding.search.CancellationSource;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.GroundCapabilities; +import ca.atlasengine.pathfinding.result.PathNode; +import ca.atlasengine.pathfinding.result.PathResult; +import ca.atlasengine.pathfinding.result.PathStatus; +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 ca.atlasengine.pathfinding.terrain.MobTraversalProfile; import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.EntityType; @@ -23,7 +32,9 @@ class SearchCancellationTest { @Test void cancellingMidSearchReturnsTheBestPartialRoute() { TestWorld world = flat(); - PathResult result = new DiscreteGroundPathfinder().findPath(cancellingAfter(world, 2000), new Pos(-30.5, 1, 0.5), new Pos(30.5, 1, 0.5), BOX, MobTraversalProfile.DEFAULT, GroundSearchLimits.ofPathLength(100), GroundCapabilities.STANDARD, control()); + PathResult result = GroundSearches.find(cancellingAfter(world, 2000), + new Pos(-30.5, 1, 0.5), new Pos(30.5, 1, 0.5), BOX, MobTraversalProfile.DEFAULT, + 100, 1, GroundCapabilities.STANDARD, control()); assertEquals(PathStatus.CANCELLED, result.status()); assertFalse(result.nodes().isEmpty()); @@ -55,7 +66,9 @@ void cancelledEntityRequestKeepsTheSamePartialRoute() { @Test void cancellingBeforeTheFirstExpansionStillReportsTheStartNode() { TestWorld world = flat(); - PathResult result = new DiscreteGroundPathfinder().findPath(cancellingAfter(world, 0), new Pos(-30.5, 1, 0.5), new Pos(30.5, 1, 0.5), BOX, MobTraversalProfile.DEFAULT, GroundSearchLimits.ofPathLength(100), GroundCapabilities.STANDARD, control()); + PathResult result = GroundSearches.find(cancellingAfter(world, 0), new Pos(-30.5, 1, 0.5), + new Pos(30.5, 1, 0.5), BOX, MobTraversalProfile.DEFAULT, 100, 1, + GroundCapabilities.STANDARD, control()); assertEquals(PathStatus.CANCELLED, result.status()); assertEquals(1, result.nodes().size(), result::toString); diff --git a/src/test/java/ca/atlasengine/pathfinding/SearchStopReasonTest.java b/src/test/java/ca/atlasengine/pathfinding/SearchStopReasonTest.java index 91a19f9..a29ed49 100644 --- a/src/test/java/ca/atlasengine/pathfinding/SearchStopReasonTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/SearchStopReasonTest.java @@ -1,7 +1,17 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import ca.atlasengine.pathfinding.profile.ClimbableCapabilities;import ca.atlasengine.pathfinding.profile.NavigationProfile;import ca.atlasengine.pathfinding.result.PathNode;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 ca.atlasengine.pathfinding.search.SearchControl;import ca.atlasengine.pathfinding.internal.movement.PartialRouteReplanner; +import ca.atlasengine.pathfinding.internal.movement.PartialRouteReplanner; import ca.atlasengine.pathfinding.internal.movement.PartialRouteReplanner.Continuation; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.ClimbableCapabilities; +import ca.atlasengine.pathfinding.profile.NavigationProfile; +import ca.atlasengine.pathfinding.result.PathNode; +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 ca.atlasengine.pathfinding.search.SearchControl; import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; @@ -166,7 +176,7 @@ void theReasonFreeOverloadKeepsTheShippedRadius() { PartialRouteReplanner replanner = radiusCase(); assertEquals(Continuation.STOP, replanner.continueLongPartialRoute( - ORIGIN, target(8), longSegment()), + ORIGIN, target(8), longSegment(), PathStop.NOT_SEARCHED), "a route with no recorded reason falls back to the radius"); } diff --git a/src/test/java/ca/atlasengine/pathfinding/ShapedGeometryAxisSweepTest.java b/src/test/java/ca/atlasengine/pathfinding/ShapedGeometryAxisSweepTest.java index 9e51ce7..2582a58 100644 --- a/src/test/java/ca/atlasengine/pathfinding/ShapedGeometryAxisSweepTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/ShapedGeometryAxisSweepTest.java @@ -1,6 +1,11 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.ClimbableCapabilities;import ca.atlasengine.pathfinding.profile.GroundCapabilities;import ca.atlasengine.pathfinding.result.PathNode;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.search.DiscreteGroundPathfinder;import ca.atlasengine.pathfinding.search.GroundSearchLimits;import ca.atlasengine.pathfinding.search.SearchControl;import ca.atlasengine.pathfinding.terrain.MobTraversalProfile; +import ca.atlasengine.pathfinding.profile.ClimbableCapabilities; +import ca.atlasengine.pathfinding.profile.GroundCapabilities; +import ca.atlasengine.pathfinding.result.PathNode; +import ca.atlasengine.pathfinding.result.PathResult; +import ca.atlasengine.pathfinding.search.SearchControl; +import ca.atlasengine.pathfinding.terrain.MobTraversalProfile; import ca.atlasengine.pathfinding.terrain.TerrainClassifier; import ca.atlasengine.pathfinding.terrain.TerrainType; import net.minestom.server.collision.BoundingBox; @@ -35,8 +40,6 @@ class ShapedGeometryAxisSweepTest { private static final BoundingBox STANDARD = new BoundingBox(0.6, 1.95, 0.6); - private final DiscreteGroundPathfinder pathfinder = - new DiscreteGroundPathfinder(); private static double[] widths() { return "all".equals(System.getProperty(WIDTH_PROPERTY)) @@ -234,8 +237,10 @@ void ladderAnchorsHangOnTheFreeSideForAllFourFacings() { "east", new int[]{1, 0}, "west", new int[]{-1, 0}); for (String facing : FACINGS) { TestWorld world = ladderTower(facing, 4); - PathResult result = pathfinder.findPath(world, new Pos(0.5 + free.get(facing)[0], - 1, 0.5 + free.get(facing)[1]), new Pos(0.5, 4, 0.5), STANDARD, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(24).reachRange(0).maxVisitedMultiplier(8).build(), climbing, SearchControl.NONE); + PathResult result = GroundSearches.find(world, new Pos(0.5 + free.get(facing)[0], + 1, 0.5 + free.get(facing)[1]), new Pos(0.5, 4, + 0.5), STANDARD, MobTraversalProfile.DEFAULT, 24, 8, climbing, + SearchControl.NONE); assertTrue(result.found(), () -> "no climb for " + facing); List climbs = result.nodes().stream().filter(node -> node.movement() == PathNode.Movement.CLIMB).toList(); @@ -288,7 +293,9 @@ private PathResult crossLane(Block fixture, int dy, double width) { private PathResult crossLane(Block fixture, int dy, double width, MobTraversalProfile profile) { - return pathfinder.findPath(corridor(fixture, dy), new Pos(0.5, 1, 0.5), new Pos(5.5, 1, 0.5), new BoundingBox(width, 1.95, width), profile, GroundSearchLimits.builder().maxPathLength(32).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.STANDARD, SearchControl.NONE); + return GroundSearches.find(corridor(fixture, dy), new Pos(0.5, 1, 0.5), + new Pos(5.5, 1, 0.5), new BoundingBox(width, 1.95, width), profile, 32, 8, + GroundCapabilities.STANDARD, SearchControl.NONE); } /** Sealed one-wide corridor: a refused cell has no detour around it. */ diff --git a/src/test/java/ca/atlasengine/pathfinding/ShapedGeometryFollowE2ETest.java b/src/test/java/ca/atlasengine/pathfinding/ShapedGeometryFollowE2ETest.java index b8dd07e..f7dbb42 100644 --- a/src/test/java/ca/atlasengine/pathfinding/ShapedGeometryFollowE2ETest.java +++ b/src/test/java/ca/atlasengine/pathfinding/ShapedGeometryFollowE2ETest.java @@ -1,8 +1,10 @@ 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.terrain.MobTraversalProfile; +import ca.atlasengine.pathfinding.profile.GroundCapabilities; +import ca.atlasengine.pathfinding.profile.NavigationMode; +import ca.atlasengine.pathfinding.profile.NavigationProfile; +import ca.atlasengine.pathfinding.terrain.MobTraversalProfile; import net.minestom.server.collision.BoundingBox; -import net.minestom.server.coordinate.ChunkRange; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.EntityCreature; import net.minestom.server.entity.EntityType; @@ -270,8 +272,10 @@ private Run drive(Env env, AsyncEntityPathfindingService service, mob.setBoundingBox(width, 1.95, width); mob.setInstance(instance, new Pos(0.5, SURFACE, 0.5)).join(); EntityNavigationController controller = - new EntityNavigationController(mob, service, - NavigationProfile.builder(NavigationMode.GROUND, MobTraversalProfile.DEFAULT, GroundCapabilities.STANDARD).allowBreaching(false).prefersShallowWater(false).avoidSun(false).build(), 0.2); + TestWorlds.controller(mob, service, + NavigationProfile.builder(NavigationMode.GROUND, + MobTraversalProfile.DEFAULT, + GroundCapabilities.STANDARD).build(), 0.2); mob.controller = controller; controller.moveTo(new Pos(6.5, SURFACE, 0.5)); int settled = 0; @@ -294,9 +298,7 @@ private Run drive(Env env, AsyncEntityPathfindingService service, /** Sealed one-wide corridor: a refused cell has no detour around it. */ private static Instance corridor(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 2, - (x, z) -> instance.loadChunk(x, z).join()); + Instance instance = TestWorlds.loadedFlat(env, 2); for (int x = -2; x <= 10; x++) { for (int y = SURFACE; y <= SURFACE + 4; y++) { instance.setBlock(x, y, 0, Block.AIR); diff --git a/src/test/java/ca/atlasengine/pathfinding/ShapedStepFollowTest.java b/src/test/java/ca/atlasengine/pathfinding/ShapedStepFollowTest.java index de1f1fd..90290de 100644 --- a/src/test/java/ca/atlasengine/pathfinding/ShapedStepFollowTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/ShapedStepFollowTest.java @@ -1,6 +1,6 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.result.PathNode;import net.minestom.server.coordinate.ChunkRange; +import ca.atlasengine.pathfinding.result.PathNode; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.EntityCreature; import net.minestom.server.entity.EntityType; @@ -262,7 +262,7 @@ private static Run follow(Env env, Instance instance, double width, mob.setBoundingBox(width, 1.95, width); mob.setInstance(instance, start).join(); EntityNavigationController controller = - EntityNavigationController.builtin(mob, service, 0.12); + TestWorlds.builtinController(mob, service, 0.12); mob.controller = controller; controller.moveTo(goal); for (int tick = 0; tick < maximumTicks; tick++) { @@ -330,9 +330,7 @@ private static int lane(double width) { } private static Instance preparedFlat(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 3, - (x, z) -> instance.loadChunk(x, z).join()); + Instance instance = TestWorlds.loadedFlat(env, 3); return instance; } diff --git a/src/test/java/ca/atlasengine/pathfinding/SharedRouteMeshTest.java b/src/test/java/ca/atlasengine/pathfinding/SharedRouteMeshTest.java index 8b46e60..6ba1ad0 100644 --- a/src/test/java/ca/atlasengine/pathfinding/SharedRouteMeshTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/SharedRouteMeshTest.java @@ -1,8 +1,12 @@ 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.NavigationPlan; +import ca.atlasengine.pathfinding.NavigationPlan; import ca.atlasengine.pathfinding.adaptive.SharedNavigationRoute; import ca.atlasengine.pathfinding.adaptive.SharedRouteMesh; +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 net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.EntityType; @@ -74,11 +78,11 @@ void builderRejectsProfileOrBoundingBoxMixingBeforePublication() { EntityType.BEE); NavigationPlan incompatibleProfile = new NavigationPlan( ground.start(), ground.target(), BOX, flying, - PathStatus.FOUND, ground.nodes(), 2, 8); + PathStatus.FOUND, ground.nodes(), 2, 8, List.of()); NavigationPlan incompatibleBox = new NavigationPlan( ground.start(), ground.target(), new BoundingBox(1.8, 2.7, 1.8), PROFILE, - PathStatus.FOUND, ground.nodes(), 2, 8); + PathStatus.FOUND, ground.nodes(), 2, 8, List.of()); SharedRouteMesh.Builder profiles = SharedRouteMesh.builder(1) @@ -106,7 +110,7 @@ void compositionRejectsSamePositionWithDifferentMovementIdentity() { PathNode.Movement.CLIMB, 2, 2, 0); NavigationPlan second = new NavigationPlan( overlap.asVec(), end.asVec(), BOX, PROFILE, - PathStatus.FOUND, List.of(changed, end), 2, 8); + PathStatus.FOUND, List.of(changed, end), 2, 8, List.of()); SharedNavigationRoute route = SharedRouteMesh.builder(1) .route("a", "b", first, 1) @@ -152,7 +156,7 @@ private static NavigationPlan withStatus( return new NavigationPlan( plan.start(), plan.target(), plan.boundingBox(), plan.profile(), status, plan.nodes(), - plan.visitedNodes(), plan.examinedNeighbors()); + plan.visitedNodes(), plan.examinedNeighbors(), List.of()); } private static NavigationPlan segment(int from, int to) { @@ -164,6 +168,6 @@ private static NavigationPlan segment(int from, int to) { PathNode.Movement.WALK, from, 1, 0), new PathNode(target.x(), 1, 0.5, PathNode.Movement.WALK, to, 1, 0)), - 2, 8); + 2, 8, List.of()); } } diff --git a/src/test/java/ca/atlasengine/pathfinding/StartNodeSamplerTest.java b/src/test/java/ca/atlasengine/pathfinding/StartNodeSamplerTest.java index 08bde3b..d45db74 100644 --- a/src/test/java/ca/atlasengine/pathfinding/StartNodeSamplerTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/StartNodeSamplerTest.java @@ -1,6 +1,7 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.search.StartNodeSampler;import net.minestom.server.collision.BoundingBox; +import ca.atlasengine.pathfinding.search.StartNodeSampler; +import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; import org.junit.jupiter.api.Test; diff --git a/src/test/java/ca/atlasengine/pathfinding/TerminalPartialTest.java b/src/test/java/ca/atlasengine/pathfinding/TerminalPartialTest.java index ad1f704..59fd1a8 100644 --- a/src/test/java/ca/atlasengine/pathfinding/TerminalPartialTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/TerminalPartialTest.java @@ -1,6 +1,7 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.metrics.NavigationMetricsSnapshot;import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;import net.minestom.server.coordinate.ChunkRange; +import ca.atlasengine.pathfinding.metrics.NavigationMetricsSnapshot; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.EntityCreature; import net.minestom.server.entity.EntityType; @@ -37,7 +38,7 @@ void anUnreachableTargetIsReportedWhereItIsDecidedAndIsNotAStall(Env env) new AsyncEntityPathfindingService(1, 8)) { ControlledCreature mob = spawn(instance, new Pos(0.5, 40, 0.5)); EntityNavigationController controller = - new EntityNavigationController(mob, service, + TestWorlds.controller(mob, service, BuiltinNavigationProfiles.forEntityType( EntityType.ZOMBIE), 0.18); mob.controller = controller; @@ -70,7 +71,7 @@ void aFollowerThatIsGivenAReachableTargetReportsNothing(Env env) new AsyncEntityPathfindingService(1, 8)) { ControlledCreature mob = spawn(instance, new Pos(0.5, 40, 0.5)); EntityNavigationController controller = - new EntityNavigationController(mob, service, + TestWorlds.controller(mob, service, BuiltinNavigationProfiles.forEntityType( EntityType.ZOMBIE), 0.18); mob.controller = controller; @@ -100,7 +101,7 @@ void aNewTargetClearsTheVerdictTheOldOneEarned(Env env) new AsyncEntityPathfindingService(1, 8)) { ControlledCreature mob = spawn(instance, new Pos(0.5, 40, 0.5)); EntityNavigationController controller = - new EntityNavigationController(mob, service, + TestWorlds.controller(mob, service, BuiltinNavigationProfiles.forEntityType( EntityType.ZOMBIE), 0.18); mob.controller = controller; @@ -127,9 +128,7 @@ private static void settle(Env env, EntityNavigationController controller) /** A sealed five-by-five pen on flat ground, with no way out. */ private static Instance pen(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 2, - (x, z) -> instance.loadChunk(x, z).join()); + Instance instance = TestWorlds.loadedFlat(env, 2); for (int offset = -3; offset <= 3; offset++) { for (int y = 40; y <= 43; y++) { instance.setBlock(3, y, offset, Block.STONE); diff --git a/src/test/java/ca/atlasengine/pathfinding/TerrainClassificationTest.java b/src/test/java/ca/atlasengine/pathfinding/TerrainClassificationTest.java index 188743f..cf60d85 100644 --- a/src/test/java/ca/atlasengine/pathfinding/TerrainClassificationTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/TerrainClassificationTest.java @@ -1,9 +1,22 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;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.result.PathNode;import ca.atlasengine.pathfinding.result.PathResult;import ca.atlasengine.pathfinding.result.PathStatus;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.adaptive.SharedMeshPolicy; -import ca.atlasengine.pathfinding.internal.adaptive.SharedMeshCoordinator; -import ca.atlasengine.pathfinding.adaptive.SharedMeshSource; import ca.atlasengine.pathfinding.NavigationPlan; +import ca.atlasengine.pathfinding.adaptive.SharedMeshPolicy; +import ca.atlasengine.pathfinding.adaptive.SharedMeshSource; +import ca.atlasengine.pathfinding.internal.adaptive.SharedMeshCoordinator; +import ca.atlasengine.pathfinding.metrics.NavigationMetrics; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +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.result.PathNode; +import ca.atlasengine.pathfinding.result.PathResult; +import ca.atlasengine.pathfinding.result.PathStatus; +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.terrain.TerrainClassification; import ca.atlasengine.pathfinding.terrain.TerrainClassifier; @@ -31,8 +44,6 @@ class TerrainClassificationTest { private static final BoundingBox NARROW = new BoundingBox(0.6, 1.8, 0.6); private static final Block CUSTOM = Block.PINK_WOOL; - private final DiscreteGroundPathfinder pathfinder = - new DiscreteGroundPathfinder(); @Test void aNamedBlockRoutesUnderTheTypeItWasNamed() { @@ -101,13 +112,12 @@ void theDefaultProfileCarriesTheDecliningConstant() { @Test void theHookReachesTheAnchoredClassifierEveryFollowerReads() { TestWorld world = new TestWorld().set(0, 0, 0, CUSTOM); - TerrainClassifier classifier = new TerrainClassifier(); - assertEquals(TerrainType.WALKABLE, classifier.classifyAnchored( + assertEquals(TerrainType.WALKABLE, TerrainClassifier.classifyAnchored( world, 0, 1, 0, NARROW, MobTraversalProfile.DEFAULT)); - assertEquals(TerrainType.DAMAGING, classifier.classifyAnchored( + assertEquals(TerrainType.DAMAGING, TerrainClassifier.classifyAnchored( world, 0, 1, 0, NARROW, naming(CUSTOM, TerrainType.DAMAGING))); - assertEquals(TerrainType.FENCE, classifier.classifyAnchored( + assertEquals(TerrainType.FENCE, TerrainClassifier.classifyAnchored( world, 0, 0, 0, NARROW, naming(CUSTOM, TerrainType.FENCE)), "select() prefers FENCE, so the diagonal rules see it too"); } @@ -130,10 +140,15 @@ void theHookReachesPlatformJumpLandings() { .set(0, 0, 0, Block.STONE) .set(3, 0, 0, CUSTOM); GroundCapabilities jumping = GroundCapabilities.STANDARD - .withPlatformJump(PlatformJumpCapabilities.builder().maxHorizontalDistance(3).maxRise(0).maxDrop(0).apexClearance(1).build()); + .withPlatformJump(PlatformJumpCapabilities.builder() + .maxHorizontalDistance(3) + .apexClearance(1) + .build()); - PathResult baseline = pathfinder.findPath(world, new Pos(0.5, 1, 0.5), new Pos(3.5, 1, 0.5), NARROW, MobTraversalProfile.DEFAULT, GroundSearchLimits.builder().maxPathLength(48).reachRange(0).maxVisitedMultiplier(8).build(), jumping, SearchControl.NONE); - PathResult named = pathfinder.findPath(world, new Pos(0.5, 1, 0.5), new Pos(3.5, 1, 0.5), NARROW, naming(CUSTOM, TerrainType.DAMAGING), GroundSearchLimits.builder().maxPathLength(48).reachRange(0).maxVisitedMultiplier(8).build(), jumping, SearchControl.NONE); + PathResult baseline = GroundSearches.find(world, new Pos(0.5, 1, 0.5), new Pos(3.5, 1, 0.5), + NARROW, MobTraversalProfile.DEFAULT, 48, 8, jumping, SearchControl.NONE); + PathResult named = GroundSearches.find(world, new Pos(0.5, 1, 0.5), new Pos(3.5, 1, 0.5), + NARROW, naming(CUSTOM, TerrainType.DAMAGING), 48, 8, jumping, SearchControl.NONE); assertTrue(baseline.found(), baseline::toString); assertEquals(PathNode.Movement.JUMP, @@ -351,14 +366,16 @@ private static TestWorld corridorPavedWith(Block block) { } private PathResult walk(TestWorld world, MobTraversalProfile profile) { - return pathfinder.findPath(world, new Pos(0.5, 1, 0.5), new Pos(8.5, 1, 0.5), NARROW, profile, GroundSearchLimits.builder().maxPathLength(64).reachRange(0).maxVisitedMultiplier(8).build(), GroundCapabilities.STANDARD, SearchControl.NONE); + return GroundSearches.find(world, new Pos(0.5, 1, 0.5), new Pos(8.5, 1, 0.5), NARROW, + profile, 64, 8, GroundCapabilities.STANDARD, SearchControl.NONE); } private static PathResult fly(TestWorld world, MobTraversalProfile mob) { return new EntityPathfinder().findPath(NavigationRequest.builder(world, new Vec(0.5, 0.5, 0.5), new Vec(8.5, 0.5, 0.5), new BoundingBox(0.6, 0.6, 0.6), - NavigationProfile.builder(NavigationMode.FLYING, mob, GroundCapabilities.STANDARD).allowBreaching(false).prefersShallowWater(false).avoidSun(false).build()) + NavigationProfile.builder(NavigationMode.FLYING, mob, + GroundCapabilities.STANDARD).build()) .maxPathLength(32).maxVisitedMultiplier(8).build(), SearchControl.NONE); } @@ -367,7 +384,8 @@ private static PathResult swim(TestWorld world, MobTraversalProfile mob) { return new EntityPathfinder().findPath(NavigationRequest.builder(world, new Vec(0.5, 1.5, 0.5), new Vec(8.5, 1.5, 0.5), new BoundingBox(0.6, 0.6, 0.6), - NavigationProfile.builder(NavigationMode.WATER, mob, GroundCapabilities.STANDARD).allowBreaching(false).prefersShallowWater(false).avoidSun(false).build()) + NavigationProfile.builder(NavigationMode.WATER, mob, + GroundCapabilities.STANDARD).build()) .maxPathLength(32).maxVisitedMultiplier(8).build(), SearchControl.NONE); } @@ -424,7 +442,7 @@ private static SharedMeshSource promoteThirdActor( (actor, request) -> plan(request), SharedMeshPolicy.builder() .promotionRequests(2).retentionRequests(1) - .build()); + .build(), new NavigationMetrics()); TestWorld world = new TestWorld(); try { coordinator.request("seed-1", "target", 1, 0, @@ -462,6 +480,6 @@ private static CompletableFuture plan( new PathNode(target.x(), target.y(), target.z(), PathNode.Movement.WALK, target.blockX(), target.blockY(), target.blockZ())), - 2, 8)); + 2, 8, List.of())); } } diff --git a/src/test/java/ca/atlasengine/pathfinding/TerrainPolicyTest.java b/src/test/java/ca/atlasengine/pathfinding/TerrainPolicyTest.java index 9fc93b3..f31800d 100644 --- a/src/test/java/ca/atlasengine/pathfinding/TerrainPolicyTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/TerrainPolicyTest.java @@ -1,6 +1,16 @@ package ca.atlasengine.pathfinding; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles;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.BuiltinMobProfiles; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +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.BuiltinMobProfiles; import ca.atlasengine.pathfinding.terrain.MobTraversalProfile; import ca.atlasengine.pathfinding.terrain.TerrainClassifier; import ca.atlasengine.pathfinding.terrain.TerrainCosts; @@ -15,7 +25,6 @@ class TerrainPolicyTest { private static final BoundingBox MOB = new BoundingBox(0.6, 1.8, 0.6); - private final TerrainClassifier classifier = new TerrainClassifier(); /** Pins the stable defaults used when a profile supplies no override. */ @Test @@ -35,17 +44,11 @@ void baselineCostsMatchTargetedGameVersion() { assertEquals(27, TerrainType.values().length); } - /** - * Every category must be priced, and the deprecated accessor must stay a - * faithful alias for the table it now delegates to. - */ + /** Every category must be priced. */ @Test - @SuppressWarnings("deprecation") void everyCategoryHasABaselineCost() { for (TerrainType type : TerrainType.values()) { assertDoesNotThrow(() -> TerrainCosts.baseline(type), type.name()); - assertEquals(TerrainCosts.baseline(type), type.defaultMalus(), - type.name()); } } @@ -86,18 +89,18 @@ void declarationOrderIsStableBecauseOrdinalsAreExported() { @Test void rawClassifierMatchesExpectedHazardFamilies() { - assertEquals(TerrainType.DAMAGING, classifier.raw(Block.CACTUS)); - assertEquals(TerrainType.DAMAGING, classifier.raw(Block.SWEET_BERRY_BUSH)); - assertEquals(TerrainType.FIRE, classifier.raw(Block.FIRE)); - assertEquals(TerrainType.FIRE, classifier.raw(Block.MAGMA_BLOCK)); - assertEquals(TerrainType.LAVA, classifier.raw(Block.LAVA)); - assertEquals(TerrainType.WATER, classifier.raw(Block.WATER)); - assertEquals(TerrainType.STICKY_HONEY, classifier.raw(Block.HONEY_BLOCK)); - assertEquals(TerrainType.DAMAGE_CAUTIOUS, classifier.raw(Block.WITHER_ROSE)); - assertEquals(TerrainType.POWDER_SNOW, classifier.raw(Block.POWDER_SNOW)); - assertEquals(TerrainType.RAIL, classifier.raw(Block.RAIL)); - assertEquals(TerrainType.FENCE, classifier.raw(Block.OAK_FENCE)); - assertEquals(TerrainType.DAMAGE_CAUTIOUS, classifier.raw(Block.SULFUR_SPIKE)); + assertEquals(TerrainType.DAMAGING, TerrainClassifier.raw(Block.CACTUS)); + assertEquals(TerrainType.DAMAGING, TerrainClassifier.raw(Block.SWEET_BERRY_BUSH)); + assertEquals(TerrainType.FIRE, TerrainClassifier.raw(Block.FIRE)); + assertEquals(TerrainType.FIRE, TerrainClassifier.raw(Block.MAGMA_BLOCK)); + assertEquals(TerrainType.LAVA, TerrainClassifier.raw(Block.LAVA)); + assertEquals(TerrainType.WATER, TerrainClassifier.raw(Block.WATER)); + assertEquals(TerrainType.STICKY_HONEY, TerrainClassifier.raw(Block.HONEY_BLOCK)); + assertEquals(TerrainType.DAMAGE_CAUTIOUS, TerrainClassifier.raw(Block.WITHER_ROSE)); + assertEquals(TerrainType.POWDER_SNOW, TerrainClassifier.raw(Block.POWDER_SNOW)); + assertEquals(TerrainType.RAIL, TerrainClassifier.raw(Block.RAIL)); + assertEquals(TerrainType.FENCE, TerrainClassifier.raw(Block.OAK_FENCE)); + assertEquals(TerrainType.DAMAGE_CAUTIOUS, TerrainClassifier.raw(Block.SULFUR_SPIKE)); } @Test @@ -106,22 +109,27 @@ void railAndFenceBelowOpenNodeBecomeWalkableRatherThanTheirRawType() { TestWorld fence = new TestWorld().set(0, 0, 0, Block.OAK_FENCE); assertEquals(TerrainType.WALKABLE, - classifier.classify(rail, new Pos(0.5, 1, 0.5), MOB, + TerrainClassifier.classify(rail, new Pos(0.5, 1, 0.5), MOB, MobTraversalProfile.DEFAULT)); assertEquals(TerrainType.WALKABLE, - classifier.classify(fence, new Pos(0.5, 1, 0.5), MOB, + TerrainClassifier.classify(fence, new Pos(0.5, 1, 0.5), MOB, MobTraversalProfile.DEFAULT)); } @Test void entityTypeRegistrySelectsSpecialProfiles() { - assertSame(MobTraversalProfile.STRIDER, BuiltinMobProfiles.forEntityType(EntityType.STRIDER)); - assertSame(MobTraversalProfile.ENDERMAN, BuiltinMobProfiles.forEntityType(EntityType.ENDERMAN)); - assertSame(MobTraversalProfile.VILLAGER, BuiltinMobProfiles.forEntityType(EntityType.VILLAGER)); + assertSame(MobTraversalProfile.STRIDER, + BuiltinMobProfiles.forEntityType(EntityType.STRIDER)); + assertSame(MobTraversalProfile.ENDERMAN, + BuiltinMobProfiles.forEntityType(EntityType.ENDERMAN)); + assertSame(MobTraversalProfile.VILLAGER, + BuiltinMobProfiles.forEntityType(EntityType.VILLAGER)); assertSame(MobTraversalProfile.FOX, BuiltinMobProfiles.forEntityType(EntityType.FOX)); - assertSame(MobTraversalProfile.WATER_ANIMAL, BuiltinMobProfiles.forEntityType(EntityType.COD)); + assertSame(MobTraversalProfile.WATER_ANIMAL, + BuiltinMobProfiles.forEntityType(EntityType.COD)); assertSame(MobTraversalProfile.BEE, BuiltinMobProfiles.forEntityType(EntityType.BEE)); - assertSame(MobTraversalProfile.SNIFFER, BuiltinMobProfiles.forEntityType(EntityType.SNIFFER)); + assertSame(MobTraversalProfile.SNIFFER, + BuiltinMobProfiles.forEntityType(EntityType.SNIFFER)); assertSame(MobTraversalProfile.CAMEL, BuiltinMobProfiles.forEntityType(EntityType.CAMEL)); assertSame(MobTraversalProfile.BREEZE, BuiltinMobProfiles.forEntityType(EntityType.BREEZE)); assertSame(MobTraversalProfile.COPPER_GOLEM, @@ -180,7 +188,9 @@ world, new Pos(0.5, 1, 0.5), new Pos(5.5, 1, 0.5), box, .maxPathLength(20) .maxVisitedMultiplier(8) .build(), SearchControl.NONE); - NavigationProfile tallButForbidden = NavigationProfile.builder(NavigationMode.GROUND, MobTraversalProfile.DEFAULT, GroundCapabilities.builder().maxStepHeight(1.5).maxFallDistance(5).allowDiagonal(true).build()).allowBreaching(false).prefersShallowWater(false).avoidSun(false).build(); + NavigationProfile tallButForbidden = NavigationProfile.builder(NavigationMode.GROUND, + MobTraversalProfile.DEFAULT, + GroundCapabilities.builder().maxStepHeight(1.5).build()).build(); PathResult noFenceCapability = pathfinder.findPath( NavigationRequest.builder( world, new Pos(0.5, 1, 0.5), @@ -205,9 +215,15 @@ void platformJumpCapabilityCannotBypassFencePermission() { world.column(x, 1, 1, 4, Block.STONE); } world.set(2, 1, 0, Block.OAK_FENCE); - GroundCapabilities jumping = GroundCapabilities.builder().maxStepHeight(1).maxFallDistance(5).allowDiagonal(true).build() - .withPlatformJump(PlatformJumpCapabilities.builder().maxHorizontalDistance(4).maxRise(1).maxDrop(2).apexClearance(1).build()); - NavigationProfile profile = NavigationProfile.builder(NavigationMode.GROUND, MobTraversalProfile.DEFAULT, jumping).allowBreaching(false).prefersShallowWater(false).avoidSun(false).build(); + GroundCapabilities jumping = GroundCapabilities.STANDARD + .withPlatformJump(PlatformJumpCapabilities.builder() + .maxHorizontalDistance(4) + .maxRise(1) + .maxDrop(2) + .apexClearance(1) + .build()); + NavigationProfile profile = NavigationProfile.builder(NavigationMode.GROUND, + MobTraversalProfile.DEFAULT, jumping).build(); PathResult result = new EntityPathfinder().findPath( NavigationRequest.builder(world, @@ -232,7 +248,7 @@ void discreteHorizontalVolumeUsesIndependentWidthAndDepth() { world.set(0, 1, 1, Block.STONE); BoundingBox nonSquare = new BoundingBox(1.8, 1.8, 0.2); - TerrainType type = classifier.classifyAnchored( + TerrainType type = TerrainClassifier.classifyAnchored( world, 0, 1, 0, nonSquare, MobTraversalProfile.DEFAULT); assertEquals(TerrainType.WALKABLE, type, @@ -259,8 +275,8 @@ void amphibiousWaterBorderIsClassifiedBeforeVolumeSelection() { .malus(TerrainType.WATER_BORDER, 4) .build(); - TerrainType type = classifier.classifyAnchoredAmphibious( - world, 0, 1, 0, wide, profile); + TerrainType type = TerrainClassifier.classifyAnchoredAmphibious( + world, 0, 1, 0, wide, profile, SearchControl.NONE); assertEquals(TerrainType.WATER_BORDER, type, "each WATER cell becomes WATER_BORDER before the complete " @@ -270,5 +286,4 @@ void amphibiousWaterBorderIsClassifiedBeforeVolumeSelection() { private static TestWorld lane() { return new TestWorld().floor(-2, 8, -3, 3, 0, Block.STONE); } - } diff --git a/src/test/java/ca/atlasengine/pathfinding/TestWorlds.java b/src/test/java/ca/atlasengine/pathfinding/TestWorlds.java new file mode 100644 index 0000000..217e555 --- /dev/null +++ b/src/test/java/ca/atlasengine/pathfinding/TestWorlds.java @@ -0,0 +1,95 @@ +package ca.atlasengine.pathfinding; + +import ca.atlasengine.pathfinding.internal.movement.NavigationRequestFactory; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.NavigationProfile; +import net.minestom.server.coordinate.ChunkRange; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.EntityCreature; +import net.minestom.server.entity.EntityType; +import net.minestom.server.instance.Instance; +import net.minestom.testing.Env; + +import java.time.Duration; +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; + +import static org.junit.jupiter.api.Assertions.fail; + +/** Live-server fixtures shared by the end-to-end tests. */ +public final class TestWorlds { + private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(10); + + private TestWorlds() { + } + + /** A flat instance with every chunk within {@code chunkRadius} of the origin loaded. */ + public static Instance loadedFlat(Env env, int chunkRadius) { + Instance instance = env.createFlatInstance(); + ChunkRange.chunksInRange(0, 0, chunkRadius, + (x, z) -> instance.loadChunk(x, z).join()); + return instance; + } + + public static EntityCreature spawn(Instance instance, EntityType type, + Pos position) { + EntityCreature creature = new EntityCreature(type); + creature.setInstance(instance, position).join(); + return creature; + } + + /** + * Ticks the controller and then the server until {@code done} holds, + * failing with {@code failure} after ten seconds of wall-clock time. + */ + public static void tickUntil(Env env, EntityNavigationController controller, + BooleanSupplier done, Supplier failure) { + long deadline = System.nanoTime() + DEFAULT_TIMEOUT.toNanos(); + while (!done.getAsBoolean()) { + if (System.nanoTime() > deadline) fail(failure.get()); + controller.tick(); + env.tick(); + } + } + + /** + * A controller submitting straight to a test-owned service, outside the + * admission layer a {@link NavigationSystem} puts in front of it, so a + * test controls the worker pool and queue it plans against. + */ + public static EntityNavigationController controller( + Entity entity, AsyncEntityPathfindingService service, + NavigationProfile profile, double movementPerTick) { + return direct(entity, service, profile, false, movementPerTick); + } + + /** As {@link #controller}, with the entity type's built-in profile. */ + public static EntityNavigationController builtinController( + Entity entity, AsyncEntityPathfindingService service, + double movementPerTick) { + return direct(entity, service, + BuiltinNavigationProfiles.forEntity(entity), true, + movementPerTick); + } + + private static EntityNavigationController direct( + Entity entity, AsyncEntityPathfindingService service, + NavigationProfile profile, boolean builtin, + double movementPerTick) { + return new EntityNavigationController(entity, service.metrics(), + profile, movementPerTick, builtin, + MovementExecutionMode.PHYSICS_VELOCITY, + NavigationRequestFactory.entityRandomOrNull(entity), + (key, request, priority) -> service.submitLatest(key, request), + new ControllerSchedulingOptions(1, service.parallelism(), + ControllerSchedulingOptions.DEFAULT_MINIMUM_SEARCH_STALL_TICKS, + ControllerSchedulingOptions.DEFAULT_SHED_BACKOFF_TICKS)); + } + + /** Ticks until the controller holds a route. */ + public static void awaitRoute(Env env, EntityNavigationController controller) { + tickUntil(env, controller, () -> !controller.nodes().isEmpty(), + () -> "no route was produced"); + } +} diff --git a/src/test/java/ca/atlasengine/pathfinding/TickOwnershipTest.java b/src/test/java/ca/atlasengine/pathfinding/TickOwnershipTest.java index f6525fa..85e1d5c 100644 --- a/src/test/java/ca/atlasengine/pathfinding/TickOwnershipTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/TickOwnershipTest.java @@ -1,7 +1,6 @@ package ca.atlasengine.pathfinding; import ca.atlasengine.pathfinding.adaptive.SharedMeshPursuit; -import net.minestom.server.coordinate.ChunkRange; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.Entity; import net.minestom.server.entity.EntityCreature; @@ -50,9 +49,7 @@ void pursuitHandsOutNoFollowerForACallerToTickAsWell() { @Test void pursuitAdvancesOncePerTickIdHoweverManyPlacesDriveIt(Env env) { - Instance instance = env.createFlatInstance(); - ChunkRange.chunksInRange(0, 0, 1, - (x, z) -> instance.loadChunk(x, z).join()); + Instance instance = TestWorlds.loadedFlat(env, 1); EntityCreature actor = new EntityCreature(EntityType.ZOMBIE); Entity target = new Entity(EntityType.ARMOR_STAND); target.setNoGravity(true); diff --git a/src/test/java/ca/atlasengine/pathfinding/WorldBoundaryE2ETest.java b/src/test/java/ca/atlasengine/pathfinding/WorldBoundaryE2ETest.java new file mode 100644 index 0000000..1f95ea9 --- /dev/null +++ b/src/test/java/ca/atlasengine/pathfinding/WorldBoundaryE2ETest.java @@ -0,0 +1,153 @@ +package ca.atlasengine.pathfinding; + +import ca.atlasengine.pathfinding.event.RouteReplanEvent; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.result.PathResult; +import ca.atlasengine.pathfinding.result.PathStatus; +import ca.atlasengine.pathfinding.result.PathStop; +import ca.atlasengine.pathfinding.search.NavigationRequest; +import net.minestom.server.coordinate.ChunkRange; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.EntityCreature; +import net.minestom.server.entity.EntityType; +import net.minestom.server.instance.Instance; +import net.minestom.server.instance.block.Block; +import net.minestom.testing.Env; +import net.minestom.testing.EnvTest; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Where navigation meets the edges of the world it is given: chunks that are + * not loaded, other instances, and edits made off the ticking thread. + */ +@EnvTest +class WorldBoundaryE2ETest { + @Test + void unloadedTerrainIsAWallRatherThanAFailedSearch(Env env) throws Exception { + Instance instance = env.createFlatInstance(); + instance.loadChunk(0, 0).join(); + EntityCreature mob = TestWorlds.spawn(instance, EntityType.ZOMBIE, + new Pos(8.5, 40, 8.5)); + env.tick(); + Vec beyondLoaded = new Vec(40.5, 40, 8.5); + + try (NavigationSystem navigation = NavigationSystem.builder() + .parallelism(1).queueCapacity(8).build()) { + PathResult direct = navigation.submit(NavigationRequest.builder( + instance, mob.getPosition(), beyondLoaded, + mob.getBoundingBox(), + BuiltinNavigationProfiles.forEntity(mob)).build()) + .get(10, TimeUnit.SECONDS); + assertEquals(PathStatus.PARTIAL, direct.status(), + "a search reaching unloaded chunks ends short of them"); + assertEquals(PathStop.TERRAIN_UNLOADED, direct.stop(), + "unloaded terrain is not proof the target is unreachable"); + + EntityNavigationController controller = navigation.controller(mob); + controller.moveTo(beyondLoaded); + for (int i = 0; i < 200; i++) { + controller.tick(); + env.tick(); + } + assertNotEquals(NavigationState.FAILED, controller.state()); + assertTrue(mob.getPosition().x() > 14, + "the mob walked to the edge of loaded terrain, at " + + mob.getPosition()); + assertEquals(0, navigation.metricsSnapshot().searches().failed(), + "no search threw"); + assertTrue(controller.targetUnreachable()); + assertEquals(PathStop.TERRAIN_UNLOADED, controller.routeStop(), + "the caller can tell an unloaded edge from a dead end"); + + // Once the terrain loads, asking again finishes the journey. + for (int chunkX = 1; chunkX <= 3; chunkX++) { + instance.loadChunk(chunkX, 0).join(); + } + controller.moveTo(beyondLoaded); + TestWorlds.tickUntil(env, controller, + () -> controller.state() == NavigationState.COMPLETED, + () -> "never arrived once loaded: " + controller.state() + + " at " + mob.getPosition()); + } finally { + mob.remove(); + } + } + + @Test + void anEditInAnotherInstanceLeavesRoutesAlone(Env env) { + Instance first = env.createFlatInstance(); + Instance second = env.createFlatInstance(); + ChunkRange.chunksInRange(0, 0, 1, (x, z) -> { + first.loadChunk(x, z).join(); + second.loadChunk(x, z).join(); + }); + EntityCreature mob = TestWorlds.spawn(first, EntityType.ZOMBIE, + new Pos(0.5, 40, 0.5)); + env.tick(); + + try (NavigationSystem navigation = NavigationSystem.builder() + .parallelism(1).queueCapacity(8).build()) { + navigation.watchBlockChanges(second); + EntityNavigationController controller = navigation.controller(mob); + controller.moveTo(new Vec(12.5, 40, 0.5)); + TestWorlds.awaitRoute(env, controller); + int before = controller.generation(); + + // The same coordinates as the route, in a different world. + for (int y = 40; y < 43; y++) second.setBlock(6, y, 0, Block.STONE); + for (int i = 0; i < 5; i++) { + controller.tick(); + env.tick(); + } + assertEquals(before, controller.generation(), + "a route is only replanned for its own instance"); + controller.close(); + } finally { + mob.remove(); + } + } + + @Test + void anEditOffTheTickThreadReplansOnIt(Env env) throws Exception { + Instance instance = TestWorlds.loadedFlat(env, 1); + EntityCreature mob = TestWorlds.spawn(instance, EntityType.ZOMBIE, + new Pos(0.5, 40, 0.5)); + env.tick(); + + List replanThreads = new CopyOnWriteArrayList<>(); + try (NavigationSystem navigation = NavigationSystem.builder() + .parallelism(1).queueCapacity(8).build()) { + navigation.eventNode().addListener(RouteReplanEvent.class, e -> { + if (e.reason() == RouteReplanEvent.Reason.BLOCK_CHANGE) { + replanThreads.add(Thread.currentThread()); + } + }); + EntityNavigationController controller = navigation.controller(mob); + controller.moveTo(new Vec(12.5, 40, 0.5)); + TestWorlds.awaitRoute(env, controller); + + Thread editor = Thread.ofPlatform().start(() -> + navigation.notifyBlockChanged(instance, new Vec(6, 40, 0))); + editor.join(); + assertTrue(replanThreads.isEmpty(), + "nothing is replanned on the editing thread"); + + controller.tick(); + assertEquals(List.of(Thread.currentThread()), replanThreads, + "the replan is announced by the tick that performs it"); + controller.close(); + } finally { + mob.remove(); + } + } +} diff --git a/src/test/java/ca/atlasengine/pathfinding/internal/search/SpatialPathfinderTest.java b/src/test/java/ca/atlasengine/pathfinding/internal/search/SpatialPathfinderTest.java index b9a9601..66554b3 100644 --- a/src/test/java/ca/atlasengine/pathfinding/internal/search/SpatialPathfinderTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/internal/search/SpatialPathfinderTest.java @@ -1,19 +1,19 @@ package ca.atlasengine.pathfinding.internal.search; +import ca.atlasengine.pathfinding.TestWorld; +import ca.atlasengine.pathfinding.influence.InfluenceResult; +import ca.atlasengine.pathfinding.influence.NavigationInfluence; import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; -import ca.atlasengine.pathfinding.search.EntityTraversalState; 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.result.PathStatus; +import ca.atlasengine.pathfinding.search.EntityTraversalState; +import ca.atlasengine.pathfinding.search.NavigationRequest; import ca.atlasengine.pathfinding.search.SearchControl; import ca.atlasengine.pathfinding.search.SpatialPathfinder; -import ca.atlasengine.pathfinding.TestWorld; -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; @@ -22,8 +22,8 @@ import net.minestom.server.instance.block.Block; import org.junit.jupiter.api.Test; -import java.util.Locale; import java.util.List; +import java.util.Locale; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -377,7 +377,7 @@ world, new Vec(0.5, 1, 0.5), new Vec(2.5, 1, 0.5), SMALL, flyingProfile()) .maxPathLength(12) .build(); - FlyNodeEvaluator evaluator = new FlyNodeEvaluator(request); + FlyNodeEvaluator evaluator = new FlyNodeEvaluator(request, SearchControl.NONE); Vec floorAdjacent = new Vec(0, 1, 0); assertEquals(1, evaluator.malusAt(floorAdjacent)); @@ -393,13 +393,16 @@ void repeatedSwimmingBreachEvaluationAccumulatesAnchorPenalty() { MobTraversalProfile swimmer = MobTraversalProfile.builder("breacher") .malus(TerrainType.BREACH, 0) .build(); - NavigationProfile profile = NavigationProfile.builder(NavigationMode.WATER, swimmer, GroundCapabilities.STANDARD).allowBreaching(true).prefersShallowWater(false).avoidSun(false).build(); + NavigationProfile profile = NavigationProfile.builder(NavigationMode.WATER, swimmer, + GroundCapabilities.STANDARD) + .allowBreaching(true) + .build(); NavigationRequest request = NavigationRequest.builder( world, new Vec(0.5, 0.5, 0.5), new Vec(0.5, 1.5, 0.5), SMALL, profile) .maxPathLength(8) .build(); - SwimNodeEvaluator evaluator = new SwimNodeEvaluator(request); + SwimNodeEvaluator evaluator = new SwimNodeEvaluator(request, SearchControl.NONE); Vec breach = new Vec(0, 1, 0); assertEquals(8, evaluator.malusAt(breach)); @@ -415,14 +418,17 @@ void wideSwimmerBreachPenaltyDependsOnAnchorFluidOnly() { MobTraversalProfile swimmer = MobTraversalProfile.builder("wide_breacher") .malus(TerrainType.BREACH, 0) .build(); - NavigationProfile profile = NavigationProfile.builder(NavigationMode.WATER, swimmer, GroundCapabilities.STANDARD).allowBreaching(true).prefersShallowWater(false).avoidSun(false).build(); + NavigationProfile profile = NavigationProfile.builder(NavigationMode.WATER, swimmer, + GroundCapabilities.STANDARD) + .allowBreaching(true) + .build(); BoundingBox wide = new BoundingBox(1.01, 0.5, 1.01); NavigationRequest request = NavigationRequest.builder( world, new Vec(0.5, 0.5, 0.5), new Vec(1.5, 1.5, 0.5), wide, profile) .maxPathLength(8) .build(); - SwimNodeEvaluator evaluator = new SwimNodeEvaluator(request); + SwimNodeEvaluator evaluator = new SwimNodeEvaluator(request, SearchControl.NONE); assertEquals(0, evaluator.malusAt(new Vec(0, 0, 0)), "a later dry cell makes the volume BREACH, but the water " @@ -439,11 +445,15 @@ private PathResult find(TestWorld world, Vec start, Vec target, } private static NavigationProfile waterProfile(boolean breach) { - return NavigationProfile.builder(NavigationMode.WATER, MobTraversalProfile.WATER_ANIMAL, GroundCapabilities.STANDARD).allowBreaching(breach).prefersShallowWater(false).avoidSun(false).build(); + return NavigationProfile.builder(NavigationMode.WATER, MobTraversalProfile.WATER_ANIMAL, + GroundCapabilities.STANDARD) + .allowBreaching(breach) + .build(); } private static NavigationProfile flyingProfile() { - return NavigationProfile.builder(NavigationMode.FLYING, MobTraversalProfile.DEFAULT, GroundCapabilities.STANDARD).allowBreaching(false).prefersShallowWater(false).avoidSun(false).build(); + return NavigationProfile.builder(NavigationMode.FLYING, MobTraversalProfile.DEFAULT, + GroundCapabilities.STANDARD).build(); } private static TestWorld waterBox(int minX, int maxX, int minY, int maxY, diff --git a/src/test/java/ca/atlasengine/pathfinding/load/LoadActor.java b/src/test/java/ca/atlasengine/pathfinding/load/LoadActor.java index 11d3069..db64ff6 100644 --- a/src/test/java/ca/atlasengine/pathfinding/load/LoadActor.java +++ b/src/test/java/ca/atlasengine/pathfinding/load/LoadActor.java @@ -1,19 +1,19 @@ package ca.atlasengine.pathfinding.load; -import ca.atlasengine.pathfinding.metrics.NavigationMetrics;import net.minestom.server.instance.block.Block; -import ca.atlasengine.pathfinding.adaptive.SharedMeshRequest; -import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; import ca.atlasengine.pathfinding.EntityNavigationController; -import ca.atlasengine.pathfinding.search.EntityTraversalState; -import ca.atlasengine.pathfinding.profile.NavigationProfile; -import ca.atlasengine.pathfinding.search.NavigationRequest; +import ca.atlasengine.pathfinding.NavigationPlan; +import ca.atlasengine.pathfinding.NavigationPlanCache; import ca.atlasengine.pathfinding.NavigationState; import ca.atlasengine.pathfinding.NavigationSystem; -import ca.atlasengine.pathfinding.result.PathStatus; import ca.atlasengine.pathfinding.adaptive.SharedMeshHandle; -import ca.atlasengine.pathfinding.NavigationPlan; -import ca.atlasengine.pathfinding.NavigationPlanCache; +import ca.atlasengine.pathfinding.adaptive.SharedMeshRequest; import ca.atlasengine.pathfinding.internal.adaptive.TargetCell; +import ca.atlasengine.pathfinding.metrics.NavigationMetrics; +import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.profile.NavigationProfile; +import ca.atlasengine.pathfinding.result.PathStatus; +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.coordinate.Pos; @@ -22,6 +22,7 @@ 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.Optional; @@ -205,7 +206,12 @@ private void submit(long tick, long worldRevision) { if (shape == Shape.PATROL && replayFromCache(worldRevision)) return; NavigationRequest request = request(instance, destination); long started = System.nanoTime(); - handle = system.sharedMesh().plan(SharedMeshRequest.builder(request).actor(entity.getUuid()).target(targetKey()).worldRevision(worldRevision).currentTick(tick).build()); + handle = system.sharedMesh().plan(SharedMeshRequest.builder(request) + .actor(entity.getUuid()) + .target(targetKey()) + .worldRevision(worldRevision) + .currentTick(tick) + .build()); metrics.dispatchTook(System.nanoTime() - started); metrics.adaptiveCall(); // Latency and outcome counts come from NavigationMetrics. The harness diff --git a/src/test/java/ca/atlasengine/pathfinding/load/LoadMetrics.java b/src/test/java/ca/atlasengine/pathfinding/load/LoadMetrics.java index c104379..a413400 100644 --- a/src/test/java/ca/atlasengine/pathfinding/load/LoadMetrics.java +++ b/src/test/java/ca/atlasengine/pathfinding/load/LoadMetrics.java @@ -1,8 +1,8 @@ package ca.atlasengine.pathfinding.load; +import ca.atlasengine.pathfinding.NavigationState; import ca.atlasengine.pathfinding.metrics.LatencyDistribution; import ca.atlasengine.pathfinding.metrics.NavigationMetricsSnapshot; -import ca.atlasengine.pathfinding.NavigationState; import java.util.ArrayList; import java.util.EnumMap; diff --git a/src/test/java/ca/atlasengine/pathfinding/load/NavigationLoadTest.java b/src/test/java/ca/atlasengine/pathfinding/load/NavigationLoadTest.java index 5b57909..1ee3027 100644 --- a/src/test/java/ca/atlasengine/pathfinding/load/NavigationLoadTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/load/NavigationLoadTest.java @@ -1,21 +1,22 @@ package ca.atlasengine.pathfinding.load; -import ca.atlasengine.pathfinding.AsyncEntityPathfindingService;import ca.atlasengine.pathfinding.adaptive.SharedMeshNavigation; -import ca.atlasengine.pathfinding.adaptive.SharedMeshRetention; -import ca.atlasengine.pathfinding.adaptive.SharedMeshRequest; -import ca.atlasengine.pathfinding.metrics.NavigationMetricsSnapshot; -import ca.atlasengine.pathfinding.search.NavigationRequest; +import ca.atlasengine.pathfinding.AsyncEntityPathfindingService; +import ca.atlasengine.pathfinding.NavigationPlanCache; import ca.atlasengine.pathfinding.NavigationState; import ca.atlasengine.pathfinding.NavigationSystem; -import ca.atlasengine.pathfinding.result.PathResult; import ca.atlasengine.pathfinding.TestWorld; -import ca.atlasengine.pathfinding.adaptive.SharedMeshPolicy; -import ca.atlasengine.pathfinding.internal.adaptive.SharedMeshCoordinator; import ca.atlasengine.pathfinding.adaptive.SharedMeshHandle; -import ca.atlasengine.pathfinding.adaptive.SharedMeshSource; -import ca.atlasengine.pathfinding.NavigationPlanCache; +import ca.atlasengine.pathfinding.adaptive.SharedMeshNavigation; import ca.atlasengine.pathfinding.adaptive.SharedMeshOptions; +import ca.atlasengine.pathfinding.adaptive.SharedMeshPolicy; +import ca.atlasengine.pathfinding.adaptive.SharedMeshRequest; +import ca.atlasengine.pathfinding.adaptive.SharedMeshRetention; +import ca.atlasengine.pathfinding.adaptive.SharedMeshSource; +import ca.atlasengine.pathfinding.internal.adaptive.SharedMeshCoordinator; +import ca.atlasengine.pathfinding.metrics.NavigationMetricsSnapshot; import ca.atlasengine.pathfinding.profile.BuiltinNavigationProfiles; +import ca.atlasengine.pathfinding.result.PathResult; +import ca.atlasengine.pathfinding.search.NavigationRequest; import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.Entity; @@ -736,7 +737,13 @@ world, new Pos(x, 40, 0.5), BuiltinNavigationProfiles.forEntityType( EntityType.ZOMBIE)) .maxPathLength(32).nodeSearchRange(32).build(); - SharedMeshHandle handle = system.sharedMesh().plan(SharedMeshRequest.builder(request).actor("actor-" + region + "-" + tick).target("target-" + region).worldRevision(1).currentTick(tick).build()); + SharedMeshHandle handle = system.sharedMesh().plan( + SharedMeshRequest.builder(request) + .actor("actor-" + region + "-" + tick) + .target("target-" + region) + .worldRevision(1) + .currentTick(tick) + .build()); if (handle.source() == SharedMeshSource.UNTRACKED_FALLBACK) { untracked++; @@ -791,7 +798,12 @@ world, new Pos(0.5, 40, 0.5), EntityType.ZOMBIE)) .maxPathLength(512).nodeSearchRange(512).build(); for (int index = 0; index < 8; index++) { - system.sharedMesh().plan(SharedMeshRequest.builder(request).actor("actor-" + index).target("target").worldRevision(1).currentTick(index).build()) + system.sharedMesh().plan(SharedMeshRequest.builder(request) + .actor("actor-" + index) + .target("target") + .worldRevision(1) + .currentTick(index) + .build()) .plan().exceptionally(failure -> null); } assertTrue(livePathfindingThreads() > 0, diff --git a/src/test/java/ca/atlasengine/pathfinding/metrics/NavigationMetricsTest.java b/src/test/java/ca/atlasengine/pathfinding/metrics/NavigationMetricsTest.java index bfe2037..0c54504 100644 --- a/src/test/java/ca/atlasengine/pathfinding/metrics/NavigationMetricsTest.java +++ b/src/test/java/ca/atlasengine/pathfinding/metrics/NavigationMetricsTest.java @@ -1,7 +1,10 @@ package ca.atlasengine.pathfinding.metrics; import ca.atlasengine.pathfinding.NavigationState; -import ca.atlasengine.pathfinding.metrics.LatencyDistribution;import ca.atlasengine.pathfinding.metrics.NavigationMetrics;import ca.atlasengine.pathfinding.metrics.NavigationMetricsSnapshot;import ca.atlasengine.pathfinding.adaptive.SharedMeshSource; +import ca.atlasengine.pathfinding.adaptive.SharedMeshSource; +import ca.atlasengine.pathfinding.metrics.LatencyDistribution; +import ca.atlasengine.pathfinding.metrics.NavigationMetrics; +import ca.atlasengine.pathfinding.metrics.NavigationMetricsSnapshot; import org.junit.jupiter.api.Test; import java.util.concurrent.CountDownLatch;