feat: Replace FastAStarAlgorithm with BitmapAStarAlgorithm (#2446)
## Summary Replaces `FastAStarAlgorithm` with `BitmapAStarAlgorithm`: one cache lookup per cell expansion (8-direction mask + per-direction destination Z) instead of 8 separate `MovementImpl.CheckMovement` calls. Adds the supporting cache infrastructure to back it. Public API unchanged — `MovementPath` / `Mobile.Move` / `CalcMoves.Find` return the same shapes; the algorithm swap is internal. ## What's in this PR - **`BitmapAStarAlgorithm`** — A* that issues one `StepCache.TryGetMask` call per cell expansion. Inline fallthrough to the per-cell slow path for multi-Z, off-map, source-Z mismatch, and non-default walkers. - **`StepCache`** — singleton chunk store keyed by `(mapId, chunkX, chunkY)`. Lazily built on first query, invalidated by `Sector.MultisVersion` mismatch, memory-bounded by sampled probabilistic LRU. - **`StepProbe`** — computes static-only walkability for a single cell, mirroring `MovementImpl.Check` minus the item / mobile collision phases. - **`StepMask` / `StepChunk`** — value / storage types for the per-cell results. - **`CacheEvictionTimer`** — periodic cap backstop (60s interval; early-returns when not over cap). - **`Map.Sector.MultisVersion`** promoted to `public` so the cache can detect dynamic-static invalidations cheaply. ## Eviction strategy Sampled probabilistic LRU (Redis-style). Per eviction, sample 5 random keys from a parallel `List<long>` kept in lockstep with the chunk dictionary; evict the oldest of the sample via swap-and-pop. O(1) per eviction regardless of resident count, so sustained cap pressure has no perpetual perf hit. ## Capability handling (interim) Non-default walkers (non-GM players, creatures with `CanSwim` / `CanFly` / `CanOpenDoors` / `CanMoveOverObstacles`) route entirely through the per-cell slow path via `BitmapAStarAlgorithm.GetSuccessorsSlowPath`. The 2-pass design (cache + capability overlay + dynamic-obstacle pass) lands in the follow-up PR.
This commit is contained in:
parent
ee1bf23b72
commit
6a3804addc
16 changed files with 1832 additions and 50 deletions
|
|
@ -0,0 +1,109 @@
|
|||
using Server.Engines.Pathing.Cache;
|
||||
using Server.Mobiles;
|
||||
using Server.PathAlgorithms.BitmapAStar;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Server.Tests.Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// Smoke tests for <see cref="BitmapAStarAlgorithm"/>'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.
|
||||
/// </summary>
|
||||
[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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plain Mobile — IsDefaultWalker returns true, the bitmap algorithm uses the cache
|
||||
/// fast path on every expansion.
|
||||
/// </summary>
|
||||
private sealed class DefaultWalkerStub : Mobile
|
||||
{
|
||||
public DefaultWalkerStub()
|
||||
{
|
||||
Body = 0xC9;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private sealed class SwimmingStub : BaseCreature
|
||||
{
|
||||
public SwimmingStub(Serial serial) : base(serial)
|
||||
{
|
||||
Body = 0xC9;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue