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
280
Projects/UOContent/Engines/Pathing/Cache/StepProbe.cs
Normal file
280
Projects/UOContent/Engines/Pathing/Cache/StepProbe.cs
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
using System;
|
||||
using CalcMoves = Server.Movement.Movement;
|
||||
|
||||
namespace Server.Engines.Pathing.Cache;
|
||||
|
||||
/// <summary>
|
||||
/// Computes static-only walkability for a single cell — the per-cell, per-direction
|
||||
/// "can step" mask and destination Z, based purely on land + statics + multis. Mirrors
|
||||
/// <see cref="MovementImpl"/>.Check minus the item and mobile collision phases.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Default-walker scope: assumes CanSwim=false, CanFly=false, CanOpenDoors=false,
|
||||
/// CantWalk=false. Single source-Z per cell. Diagonal corner-cut is NOT applied here;
|
||||
/// callers must AND the partner-cell results at query time per the creature rule
|
||||
/// (one cardinal partner walkable suffices).
|
||||
/// </remarks>
|
||||
public static class StepProbe
|
||||
{
|
||||
private const int PersonHeight = 16;
|
||||
private const int StepHeight = 2;
|
||||
|
||||
public static StepMask ComputeMaskAt(Map map, int x, int y, sbyte sourceZ)
|
||||
{
|
||||
if (map == null || map == Map.Internal)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
GetStaticStartZ(map, x, y, sourceZ, out var startZ, out var startTop, out _);
|
||||
|
||||
byte mask = 0;
|
||||
Span<sbyte> destZs = stackalloc sbyte[8];
|
||||
|
||||
for (var d = 0; d < 8; d++)
|
||||
{
|
||||
var dx = x;
|
||||
var dy = y;
|
||||
CalcMoves.Offset((Direction)d, ref dx, ref dy);
|
||||
|
||||
if (CheckStaticStep(map, dx, dy, startZ, startTop, out var newZ))
|
||||
{
|
||||
mask |= (byte)(1 << d);
|
||||
destZs[d] = (sbyte)newZ;
|
||||
}
|
||||
}
|
||||
|
||||
return new StepMask(
|
||||
mask,
|
||||
destZs[0], destZs[1], destZs[2], destZs[3],
|
||||
destZs[4], destZs[5], destZs[6], destZs[7]
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the slow path's standing-Z for a default walker at (x, y) with hint locZ.
|
||||
/// This is the Z the creature ends up STANDING AT — typically the topmost walkable
|
||||
/// surface that's reachable from locZ (paver Z+1 for paver-over-ground; landCenter
|
||||
/// for bare land). Mirrors MovementImpl.Check's surface-selection logic for the
|
||||
/// destination cell, distilled to "what Z value does the slow path return as newZ
|
||||
/// when stepping ONTO this cell". Used by StepCache to bake SourceZ
|
||||
/// correctly so A*'s tracked-per-cell Z matches the cache's bake-time assumption.
|
||||
/// </summary>
|
||||
public static int ComputeStandingZ(Map map, int x, int y, int locZ)
|
||||
{
|
||||
GetStaticStartZ(map, x, y, locZ, out _, out _, out var zCenter);
|
||||
return zCenter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors GetStartZ from MovementImpl, but static-only (no item list).
|
||||
/// Assumes default walker: CanSwim=false, CantWalk=false.
|
||||
/// </summary>
|
||||
private static void GetStaticStartZ(Map map, int x, int y, int locZ, out int zLow, out int zTop, out int zCenter)
|
||||
{
|
||||
var landTile = map.Tiles.GetLandTile(x, y);
|
||||
var flags = TileData.LandTable[landTile.ID & TileData.MaxLandValue].Flags;
|
||||
var impassable = (flags & TileFlag.Impassable) != 0;
|
||||
|
||||
// CantWalk=false, CanSwim=false → landBlocks = impassable
|
||||
var landBlocks = impassable;
|
||||
|
||||
map.GetAverageZ(x, y, out var landZ, out var landCenter, out var landTop);
|
||||
|
||||
var considerLand = !landTile.Ignored;
|
||||
|
||||
zCenter = zLow = zTop = 0;
|
||||
var isSet = false;
|
||||
|
||||
if (considerLand && !landBlocks && locZ >= landCenter)
|
||||
{
|
||||
zLow = landZ;
|
||||
zCenter = landCenter;
|
||||
zTop = landTop;
|
||||
isSet = true;
|
||||
}
|
||||
|
||||
foreach (var tile in map.Tiles.GetStaticAndMultiTiles(x, y))
|
||||
{
|
||||
var id = TileData.ItemTable[tile.ID & TileData.MaxItemValue];
|
||||
var calcTop = tile.Z + id.CalcHeight;
|
||||
|
||||
// CanSwim=false → only check Surface; CantWalk=false
|
||||
if (isSet && calcTop < zCenter || locZ < calcTop || !id.Surface)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
zLow = tile.Z;
|
||||
zCenter = calcTop;
|
||||
|
||||
var top = tile.Z + id.Height;
|
||||
|
||||
if (!isSet || top > zTop)
|
||||
{
|
||||
zTop = top;
|
||||
}
|
||||
|
||||
isSet = true;
|
||||
}
|
||||
|
||||
if (!isSet)
|
||||
{
|
||||
zLow = zTop = locZ;
|
||||
}
|
||||
else if (locZ > zTop)
|
||||
{
|
||||
zTop = locZ;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors MovementImpl.Check for static tiles only.
|
||||
/// Assumes default walker: CanSwim=false, CanFly=false, CantWalk=false,
|
||||
/// AlwaysIgnoreDoors=false. Items and mobile collision phases are omitted.
|
||||
/// </summary>
|
||||
private static bool CheckStaticStep(Map map, int x, int y, int startZ, int startTop, out int newZ)
|
||||
{
|
||||
newZ = 0;
|
||||
|
||||
if (x < 0 || y < 0 || x >= map.Width || y >= map.Height)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var landTile = map.Tiles.GetLandTile(x, y);
|
||||
var flags = TileData.LandTable[landTile.ID & TileData.MaxLandValue].Flags;
|
||||
var impassable = (flags & TileFlag.Impassable) != 0;
|
||||
|
||||
// CantWalk=false, CanSwim=false → landBlocks = impassable
|
||||
var landBlocks = impassable;
|
||||
|
||||
var considerLand = !landTile.Ignored;
|
||||
|
||||
map.GetAverageZ(x, y, out var landZ, out var landCenter, out _);
|
||||
|
||||
var moveIsOk = false;
|
||||
|
||||
var stepTop = startTop + StepHeight;
|
||||
var checkTop = startZ + PersonHeight;
|
||||
|
||||
int testTop;
|
||||
|
||||
foreach (var tile in map.Tiles.GetStaticAndMultiTiles(x, y))
|
||||
{
|
||||
var itemData = TileData.ItemTable[tile.ID & TileData.MaxItemValue];
|
||||
|
||||
// CanSwim=false, CantWalk=false:
|
||||
// Skip if not a passable surface (no swim path either)
|
||||
if (!itemData.Surface || itemData.Impassable)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var itemZ = tile.Z;
|
||||
var itemTop = itemZ;
|
||||
var ourZ = itemZ + itemData.CalcHeight;
|
||||
testTop = checkTop;
|
||||
|
||||
// Pick the candidate closest to startZ; ties broken by higher ourZ
|
||||
if (moveIsOk)
|
||||
{
|
||||
var cmp = Math.Abs(ourZ - startZ) - Math.Abs(newZ - startZ);
|
||||
|
||||
if (cmp > 0 || cmp == 0 && ourZ > newZ)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (ourZ + PersonHeight > testTop)
|
||||
{
|
||||
testTop = ourZ + PersonHeight;
|
||||
}
|
||||
|
||||
if (!itemData.Bridge)
|
||||
{
|
||||
itemTop += itemData.Height;
|
||||
}
|
||||
|
||||
if (stepTop < itemTop)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var landCheck = itemZ + Math.Min(itemData.Height, StepHeight);
|
||||
|
||||
if (considerLand && landCheck < landCenter && landCenter > ourZ && testTop > landZ)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// IsOk equivalent: check static tiles don't block (ourZ, testTop) space
|
||||
if (StaticsBlockAt(map, x, y, ourZ, testTop))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
newZ = ourZ;
|
||||
moveIsOk = true;
|
||||
}
|
||||
|
||||
// Land surface fallback (mirrors Check's land block at the bottom)
|
||||
if (!considerLand || landBlocks || stepTop < landZ)
|
||||
{
|
||||
return moveIsOk;
|
||||
}
|
||||
|
||||
testTop = checkTop;
|
||||
|
||||
if (landCenter + PersonHeight > testTop)
|
||||
{
|
||||
testTop = landCenter + PersonHeight;
|
||||
}
|
||||
|
||||
var shouldCheck = true;
|
||||
|
||||
if (moveIsOk)
|
||||
{
|
||||
var cmp = Math.Abs(landCenter - startZ) - Math.Abs(newZ - startZ);
|
||||
|
||||
if (cmp > 0 || cmp == 0 && landCenter > newZ)
|
||||
{
|
||||
shouldCheck = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldCheck && !StaticsBlockAt(map, x, y, landCenter, testTop))
|
||||
{
|
||||
newZ = landCenter;
|
||||
moveIsOk = true;
|
||||
}
|
||||
|
||||
return moveIsOk;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors the static-tile portion of IsOk: returns true if any static tile at (x,y)
|
||||
/// has ImpassableSurface and overlaps the vertical range (ourZ, testTop).
|
||||
/// </summary>
|
||||
private static bool StaticsBlockAt(Map map, int x, int y, int ourZ, int testTop)
|
||||
{
|
||||
foreach (var check in map.Tiles.GetStaticAndMultiTiles(x, y))
|
||||
{
|
||||
var itemData = TileData.ItemTable[check.ID & TileData.MaxItemValue];
|
||||
|
||||
if (itemData.ImpassableSurface)
|
||||
{
|
||||
var checkZ = check.Z;
|
||||
var checkTop = checkZ + itemData.CalcHeight;
|
||||
|
||||
if (checkTop > ourZ && testTop > checkZ)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue