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;