using Server.Engines.Pathing.Cache; using Server.Mobiles; using Server.PathAlgorithms.BitmapAStar; using Xunit; using Xunit.Abstractions; namespace Server.Tests.Pathfinding; /// /// Smoke tests for 's two branches: cache-direct fast /// path (default walkers) and per-cell slow path (capability creatures and non-GM players). /// Exercised end-to-end against the real Trammel TileMatrix to ensure neither regresses /// to "no path found" on reachable goals. /// [Collection("Sequential Pathfinding Tests")] public class BitmapAStarAlgorithmTests { private readonly ITestOutputHelper _output; public BitmapAStarAlgorithmTests(ITestOutputHelper output) { _output = output; } [Theory] // Pinned cell (1500, 1600, z=10): mask=0xC1 → N, W, NW walkable. // Use start.Z for goal.Z so destNode lands in the same Z plane the algorithm reaches // during expansion (GetAverageZ at the goal cell may differ from the engine's // runtime-computed standing Z, which would break the destNode equality check). [InlineData(1500, 1600, 1498, 1598)] // NW, 2 cells diagonal [InlineData(1500, 1600, 1497, 1599)] // NW-ish, 3 W + 1 N public void DefaultWalker_FindsPath_ViaCacheFastPath(int sx, int sy, int gx, int gy) { StepCache.Instance.Clear(); var map = Map.Maps[1]; Assert.NotNull(map); var stub = new DefaultWalkerStub(); map.GetAverageZ(sx, sy, out _, out var startZ, out _); var start = new Point3D(sx, sy, (sbyte)startZ); var goal = new Point3D(gx, gy, (sbyte)startZ); stub.MoveToWorld(start, map); var result = BitmapAStarAlgorithm.Instance.Find(stub, map, start, goal); stub.Delete(); Assert.NotNull(result); Assert.NotEmpty(result); _output.WriteLine($"default-walker ({sx},{sy})->({gx},{gy}): {result.Length} steps"); } [Theory] [InlineData(1500, 1600, 1498, 1598)] // NW, 2 cells diagonal [InlineData(1500, 1600, 1497, 1599)] // NW-ish, 3 W + 1 N public void CapabilityCreature_FindsPath_ViaInlineSlowPath(int sx, int sy, int gx, int gy) { StepCache.Instance.Clear(); var map = Map.Maps[1]; Assert.NotNull(map); var stub = new SwimmingStub(World.NewMobile); stub.DefaultMobileInit(); stub.CanSwim = true; // forces non-default-walker → inline GetSuccessorsSlowPath map.GetAverageZ(sx, sy, out _, out var startZ, out _); var start = new Point3D(sx, sy, (sbyte)startZ); var goal = new Point3D(gx, gy, (sbyte)startZ); stub.MoveToWorld(start, map); var result = BitmapAStarAlgorithm.Instance.Find(stub, map, start, goal); stub.Delete(); // Capability creature: assert reachability — exact length depends on terrain and // tie-breaking, but a swimmer should always reach a goal a default walker reaches // on dry land (CanSwim is permissive, never restrictive). Assert.NotNull(result); Assert.NotEmpty(result); _output.WriteLine($"swimmer ({sx},{sy})->({gx},{gy}): {result.Length} steps"); } /// /// Plain Mobile — IsDefaultWalker returns true, the bitmap algorithm uses the cache /// fast path on every expansion. /// private sealed class DefaultWalkerStub : Mobile { public DefaultWalkerStub() { Body = 0xC9; } } /// /// BaseCreature with CanSwim=true — IsDefaultWalker returns false, the bitmap /// algorithm short-circuits GetSuccessors to GetSuccessorsSlowPath on every cell. /// Use the Serial constructor (deserialization path) to bypass NPCSpeeds init, /// which requires the npc-speeds.json table loaded — not available in tests. /// private sealed class SwimmingStub : BaseCreature { public SwimmingStub(Serial serial) : base(serial) { Body = 0xC9; } } }