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
91
Projects/UOContent.Tests/Fixtures/PathfindingTestFixture.cs
Normal file
91
Projects/UOContent.Tests/Fixtures/PathfindingTestFixture.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using Server.Items;
|
||||
using Server.Misc;
|
||||
using Server.Movement;
|
||||
using Server.Tests.Maps;
|
||||
using Xunit;
|
||||
|
||||
namespace Server.Tests.Pathfinding;
|
||||
|
||||
[CollectionDefinition("Sequential Pathfinding Tests", DisableParallelization = true)]
|
||||
public class PathfindingTestFixture : ICollectionFixture<PathfindingTestFixture>, IDisposable
|
||||
{
|
||||
public PathfindingTestFixture()
|
||||
{
|
||||
Core.ApplicationAssembly = Assembly.GetExecutingAssembly();
|
||||
Core.LoopContext = new EventLoopContext();
|
||||
Core.Expansion = Expansion.EJ;
|
||||
|
||||
ServerConfiguration.Load(true);
|
||||
ServerConfiguration.AssemblyDirectories.Add(Core.BaseDirectory);
|
||||
|
||||
var clientFiles = Environment.GetEnvironmentVariable("MODERNUO_TEST_DATA_DIR")
|
||||
?? @"C:\Ultima Online Classic";
|
||||
ServerConfiguration.DataDirectories.Add(clientFiles);
|
||||
|
||||
AssemblyHandler.LoadAssemblies(["Server.dll", "UOContent.dll"]);
|
||||
|
||||
SkillsInfo.Configure();
|
||||
Server.Network.NetState.Configure();
|
||||
TestMapDefinitions.ConfigureTestMapDefinitions();
|
||||
|
||||
World.Configure();
|
||||
Timer.Init(0);
|
||||
RaceDefinitions.Configure();
|
||||
MovementImpl.Configure();
|
||||
World.Load();
|
||||
World.ExitSerializationThreads();
|
||||
DecayScheduler.Configure();
|
||||
|
||||
// TileData's static cctor short-circuits when running under xUnit
|
||||
// (see Server/TileData.cs:295). Force-load via reflection so LandTable/ItemTable
|
||||
// flags are populated; without this, every tile reads as flag=None and
|
||||
// MovementImpl.CheckMovement treats everything as walkable.
|
||||
ForceLoadTileData();
|
||||
|
||||
VerifyTrammelTileDataLoaded();
|
||||
}
|
||||
|
||||
private static void ForceLoadTileData()
|
||||
{
|
||||
var loadMethod = typeof(TileData).GetMethod(
|
||||
"Load",
|
||||
BindingFlags.Static | BindingFlags.NonPublic
|
||||
);
|
||||
if (loadMethod == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"TileData.Load not found via reflection — engine may have refactored."
|
||||
);
|
||||
}
|
||||
loadMethod.Invoke(null, null);
|
||||
}
|
||||
|
||||
private static void VerifyTrammelTileDataLoaded()
|
||||
{
|
||||
var trammel = Map.Maps[1];
|
||||
if (trammel == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Trammel (mapId=1) was not registered. Check TestMapDefinitions."
|
||||
);
|
||||
}
|
||||
|
||||
var tile = trammel.Tiles.GetLandTile(1500, 1600);
|
||||
if (tile.ID == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Trammel tile data did not load — GetLandTile(1500,1600) returned ID 0. " +
|
||||
$"Verify Distribution/Data/map1*.mul (or map1LegacyMUL.uop) is present at " +
|
||||
$"{Path.Combine(Core.BaseDirectory, "Data")}."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Timer.Init(0);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue