ModernUO/Projects/UOContent.Tests/Fixtures/PathfindingTestFixture.cs
Kamron Batman 6a3804addc
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.
2026-05-05 21:53:43 -07:00

91 lines
2.9 KiB
C#

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);
}
}