From 30fec7da26c4df18ab761c7c352daa19594f3ff7 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sun, 7 Jun 2026 13:25:10 -0700 Subject: [PATCH] fix: Cleans up AI Pathfinding code to make it more portable for custom requirements. (#2474) --- .../Fixtures/TestServerInitializer.cs | 7 ++ .../Pathing/BitmapAStarAlgorithmTests.cs | 6 +- .../Engines/Pathing/PathfindRecorderTests.cs | 24 ++--- .../Engines/Pathing/StepCacheFileTests.cs | 20 ++--- .../Engines/Pathing/StepCacheFileV6Tests.cs | 14 +-- .../Engines/Pathing/StepCacheFileV7Tests.cs | 6 +- .../Engines/Pathing/StepCacheFileV8Tests.cs | 6 +- .../Pathing/StepCacheLifecycleTests.cs | 2 +- .../Engines/Pathing/StepCacheParityTests.cs | 9 +- .../Engines/Pathing/StepProbeParityTests.cs | 2 +- .../Engines/Pathing/BitmapAStarAlgorithm.cs | 72 ++++++++++----- .../Engines/Pathing/Cache/StepCache.cs | 88 ++++++++++++++++++- .../Engines/Pathing/Cache/StepCacheFile.cs | 4 +- .../UOContent/Engines/Pathing/MovementPath.cs | 1 - .../UOContent/Engines/Pathing/PathDiag.cs | 2 +- .../Engines/Pathing/PathfindRecorder.cs | 37 ++++---- 16 files changed, 198 insertions(+), 102 deletions(-) diff --git a/Projects/UOContent.Tests/Fixtures/TestServerInitializer.cs b/Projects/UOContent.Tests/Fixtures/TestServerInitializer.cs index a64286813..888baa83f 100644 --- a/Projects/UOContent.Tests/Fixtures/TestServerInitializer.cs +++ b/Projects/UOContent.Tests/Fixtures/TestServerInitializer.cs @@ -5,6 +5,7 @@ using System.Threading; using Server.Items; using Server.Misc; using Server.Movement; +using Server.PathAlgorithms; using Server.Tests.Maps; namespace Server.Tests; @@ -56,6 +57,12 @@ internal static class TestServerInitializer Server.Network.NetState.Configure(); TestMapDefinitions.ConfigureTestMapDefinitions(); + // Production runs every static Configure() via AssemblyHandler.Invoke("Configure"); + // the fixture calls a curated subset, so configure the pathfinding singleton here so + // BitmapAStarAlgorithm.Instance carries its configured MaxSearchNodes before any test + // calls Find. ServerConfiguration is already loaded above, so the setting resolves. + BitmapAStarAlgorithm.Configure(); + World.Configure(); Timer.Init(0); RaceDefinitions.Configure(); diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/BitmapAStarAlgorithmTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/BitmapAStarAlgorithmTests.cs index f92fab0c9..ab9497729 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Pathing/BitmapAStarAlgorithmTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/BitmapAStarAlgorithmTests.cs @@ -1,6 +1,6 @@ using Server.Engines.Pathing.Cache; using Server.Mobiles; -using Server.PathAlgorithms.BitmapAStar; +using Server.PathAlgorithms; using Server.Systems.FeatureFlags; using Xunit; using Xunit.Abstractions; @@ -121,7 +121,7 @@ public class BitmapAStarAlgorithmTests var y = sy; foreach (var dir in result) { - Server.Movement.Movement.Offset(dir, ref x, ref y); + Movement.Movement.Offset(dir, ref x, ref y); Assert.False(x == blockX && y == blockY, $"path traversed blocker cell ({blockX},{blockY})"); } @@ -180,7 +180,7 @@ public class BitmapAStarAlgorithmTests var y = sy; foreach (var dir in result) { - Server.Movement.Movement.Offset(dir, ref x, ref y); + Movement.Movement.Offset(dir, ref x, ref y); Assert.False(x == blockX && y == blockY, $"path traversed item-blocker cell ({blockX},{blockY})"); } diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/PathfindRecorderTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/PathfindRecorderTests.cs index da44a4f19..0930bad7a 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Pathing/PathfindRecorderTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/PathfindRecorderTests.cs @@ -10,22 +10,11 @@ public class PathfindRecorderTests private static string NewTempPath() => Path.Combine(Path.GetTempPath(), $"pathfind-recorder-{System.Guid.NewGuid():N}.jsonl"); - /// - /// Reflection-set the static _outputPath without going through Configure (which - /// reads from server.cfg) so tests don't poison the project's server.cfg. - /// - private static void OverrideOutputPath(string path) - { - typeof(PathfindRecorder).GetField("_outputPath", - System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)! - .SetValue(null, path); - } - [Fact] public void Disabled_RecordIfEnabled_DoesNothing() { var path = NewTempPath(); - OverrideOutputPath(path); + PathfindRecorder.OutputPath = path; PathfindRecorder.SetEnabled(false); try @@ -52,7 +41,7 @@ public class PathfindRecorderTests public void Enabled_RecordIfEnabled_WritesValidJsonlLine() { var path = NewTempPath(); - OverrideOutputPath(path); + PathfindRecorder.OutputPath = path; PathfindRecorder.SetEnabled(true); try @@ -95,7 +84,7 @@ public class PathfindRecorderTests public void Enabled_RecordsCapabilityFlagsFromBaseCreature() { var path = NewTempPath(); - OverrideOutputPath(path); + PathfindRecorder.OutputPath = path; PathfindRecorder.SetEnabled(true); try @@ -130,7 +119,7 @@ public class PathfindRecorderTests public void SetEnabled_TogglingTwice_IsIdempotent() { var path = NewTempPath(); - OverrideOutputPath(path); + PathfindRecorder.OutputPath = path; try { @@ -155,9 +144,6 @@ public class PathfindRecorderTests private sealed class RecorderStub : Server.Mobiles.BaseCreature { - public RecorderStub(Serial serial) : base(serial) - { - Body = 0xC9; - } + public RecorderStub(Serial serial) : base(serial) => Body = 0xC9; } } diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileTests.cs index d8e9aed01..ea3468759 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileTests.cs @@ -55,7 +55,7 @@ public class StepCacheFileTests expected[i] = cache.TryGetMask(map, x, y, standZ[i]); } - var path = Path.Combine(Path.GetTempPath(), $"step-cache-roundtrip-{System.Guid.NewGuid():N}.swb"); + var path = Path.Combine(Path.GetTempPath(), $"step-cache-roundtrip-{Guid.NewGuid():N}.swb"); try { var written = cache.SaveToFile(path, map.MapID); @@ -109,7 +109,7 @@ public class StepCacheFileTests var cache = StepCache.Instance; cache.Clear(); - var path = Path.Combine(Path.GetTempPath(), $"step-cache-missing-{System.Guid.NewGuid():N}.swb"); + var path = Path.Combine(Path.GetTempPath(), $"step-cache-missing-{Guid.NewGuid():N}.swb"); Assert.False(cache.TryOpenLazyReader(path, mapId: 1)); Assert.Equal(0, cache.OpenLazyReaderCount); } @@ -120,7 +120,7 @@ public class StepCacheFileTests var cache = StepCache.Instance; cache.Clear(); - var path = Path.Combine(Path.GetTempPath(), $"step-cache-badmagic-{System.Guid.NewGuid():N}.swb"); + var path = Path.Combine(Path.GetTempPath(), $"step-cache-badmagic-{Guid.NewGuid():N}.swb"); try { File.WriteAllBytes(path, new byte[] @@ -150,7 +150,7 @@ public class StepCacheFileTests var map = Map.Maps[1]; cache.TryGetMask(map, 1500, 1600, sourceZ: 10); - var path = Path.Combine(Path.GetTempPath(), $"step-cache-stalehash-{System.Guid.NewGuid():N}.swb"); + var path = Path.Combine(Path.GetTempPath(), $"step-cache-stalehash-{Guid.NewGuid():N}.swb"); try { cache.SaveToFile(path, map.MapID); @@ -193,7 +193,7 @@ public class StepCacheFileTests Assert.NotNull(map); // Populate a handful of chunks. - var coords = new (int, int)[] + var coords = new[] { (1500, 1600), (1516, 1600), (1500, 1616), (1516, 1616), (1532, 1600) }; @@ -202,7 +202,7 @@ public class StepCacheFileTests cache.TryGetMask(map, x, y, sourceZ: 10); } - var path = Path.Combine(Path.GetTempPath(), $"step-cache-lazy-{System.Guid.NewGuid():N}.swb"); + var path = Path.Combine(Path.GetTempPath(), $"step-cache-lazy-{Guid.NewGuid():N}.swb"); try { Assert.Equal(coords.Length, cache.SaveToFile(path, map.MapID)); @@ -269,7 +269,7 @@ public class StepCacheFileTests chunk.SwimZE_Layer[cellIndex] = -7; chunk.SwimZSE_Layer[cellIndex] = -7; - var path = Path.Combine(Path.GetTempPath(), $"step-cache-swim-{System.Guid.NewGuid():N}.swb"); + var path = Path.Combine(Path.GetTempPath(), $"step-cache-swim-{Guid.NewGuid():N}.swb"); try { Assert.Equal(1, cache.SaveToFile(path, map.MapID)); @@ -314,7 +314,7 @@ public class StepCacheFileTests var map = Map.Maps[1]; Assert.NotNull(map); - var coords = new (int, int)[] + var coords = new[] { (1500, 1600), (1516, 1600), (1500, 1616), (1516, 1616), (1532, 1600) }; @@ -323,7 +323,7 @@ public class StepCacheFileTests cache.TryGetMask(map, x, y, sourceZ: 10); } - var path = Path.Combine(Path.GetTempPath(), $"step-cache-preload-{System.Guid.NewGuid():N}.swb"); + var path = Path.Combine(Path.GetTempPath(), $"step-cache-preload-{Guid.NewGuid():N}.swb"); try { Assert.Equal(coords.Length, cache.SaveToFile(path, map.MapID)); @@ -369,7 +369,7 @@ public class StepCacheFileTests // Build + save one chunk. cache.TryGetMask(map, 1500, 1600, sourceZ: 10); - var path = Path.Combine(Path.GetTempPath(), $"step-cache-bypass-{System.Guid.NewGuid():N}.swb"); + var path = Path.Combine(Path.GetTempPath(), $"step-cache-bypass-{Guid.NewGuid():N}.swb"); try { Assert.Equal(1, cache.SaveToFile(path, map.MapID)); diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileV6Tests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileV6Tests.cs index 30d8630c1..24764c272 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileV6Tests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileV6Tests.cs @@ -48,8 +48,8 @@ public class StepCacheFileV6Tests for (var i = 0; i < StepChunk.CellsPerChunk; i++) { c.WalkMask[i] = (byte)(i & 0xFF); - c.WetMask[i] = (byte)((~i) & 0xFF); - c.SourceZ[i] = (sbyte)(baseZ + (i % 7) - 3); // varies, mostly != 0 + c.WetMask[i] = (byte)(~i & 0xFF); + c.SourceZ[i] = (sbyte)(baseZ + i % 7 - 3); // varies, mostly != 0 } SetFlatDirectional(c); return c; @@ -78,9 +78,9 @@ public class StepCacheFileV6Tests { c.WalkMask[i] = (byte)(i & 0xFF); c.WetMask[i] = (byte)((i * 7) & 0xFF); - c.SourceZ[i] = (sbyte)((i % 40) - 20); - c.WalkZN[i] = (sbyte)(c.SourceZ[i] + (i % 3)); - c.SwimZS[i] = (sbyte)(c.SourceZ[i] - (i % 2)); + c.SourceZ[i] = (sbyte)(i % 40 - 20); + c.WalkZN[i] = (sbyte)(c.SourceZ[i] + i % 3); + c.SwimZS[i] = (sbyte)(c.SourceZ[i] - i % 2); } return c; } @@ -91,10 +91,10 @@ public class StepCacheFileV6Tests c.AllocateSwimLayer(); for (var i = 0; i < StepChunk.CellsPerChunk; i++) { - c.SwimSourceZ[i] = (sbyte)((i % 30) - 15); + c.SwimSourceZ[i] = (sbyte)(i % 30 - 15); c.SwimMask[i] = (byte)((i * 5) & 0xFF); c.SwimZN_Layer[i] = (sbyte)(i % 7); - c.SwimZNW_Layer[i] = (sbyte)(-(i % 4)); + c.SwimZNW_Layer[i] = (sbyte)-(i % 4); } return c; } diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileV7Tests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileV7Tests.cs index 83a44d46a..f5e517972 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileV7Tests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileV7Tests.cs @@ -18,9 +18,9 @@ public class StepCacheFileV7Tests { c.WalkMask[i] = (byte)(i & 0xFF); c.WetMask[i] = (byte)((i * 7) & 0xFF); - c.SourceZ[i] = (sbyte)((i % 40) - 20); - c.WalkZN[i] = (sbyte)(c.SourceZ[i] + (i % 3)); - c.SwimZS[i] = (sbyte)(c.SourceZ[i] - (i % 2)); + c.SourceZ[i] = (sbyte)(i % 40 - 20); + c.WalkZN[i] = (sbyte)(c.SourceZ[i] + i % 3); + c.SwimZS[i] = (sbyte)(c.SourceZ[i] - i % 2); } return c; } diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileV8Tests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileV8Tests.cs index 0a391fb39..8d3cd495b 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileV8Tests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileV8Tests.cs @@ -18,9 +18,9 @@ public class StepCacheFileV8Tests { c.WalkMask[i] = (byte)((i + seed) & 0xFF); c.WetMask[i] = (byte)((i * 7 + seed) & 0xFF); - c.SourceZ[i] = (sbyte)(((i + seed) % 40) - 20); - c.WalkZN[i] = (sbyte)(c.SourceZ[i] + (i % 3)); - c.SwimZS[i] = (sbyte)(c.SourceZ[i] - (i % 2)); + c.SourceZ[i] = (sbyte)((i + seed) % 40 - 20); + c.WalkZN[i] = (sbyte)(c.SourceZ[i] + i % 3); + c.SwimZS[i] = (sbyte)(c.SourceZ[i] - i % 2); } return c; } diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheLifecycleTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheLifecycleTests.cs index de4b2fa51..72531ba97 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheLifecycleTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheLifecycleTests.cs @@ -499,7 +499,7 @@ public class StepCacheLifecycleTests // Build 5 distinct chunks by querying different sectors. for (var i = 0; i < 5; i++) { - var x = 1500 + (i * 16); + var x = 1500 + i * 16; var y = 1600; cache.TryGetMask(map, x, y, 10); System.Threading.Thread.Sleep(2); // ensure LastTouchedTicks differs diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheParityTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheParityTests.cs index b00d30f8f..149a9fe13 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheParityTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheParityTests.cs @@ -83,10 +83,11 @@ public class StepCacheParityTests wetCells++; } - if (lookup.WalkZ_N != baker.WalkZ_N || lookup.WalkZ_NE != baker.WalkZ_NE - || lookup.WalkZ_E != baker.WalkZ_E || lookup.WalkZ_SE != baker.WalkZ_SE - || lookup.WalkZ_S != baker.WalkZ_S || lookup.WalkZ_SW != baker.WalkZ_SW - || lookup.WalkZ_W != baker.WalkZ_W || lookup.WalkZ_NW != baker.WalkZ_NW) + if (lookup.WalkZ_N != baker.WalkZ_N + || lookup.WalkZ_NE != baker.WalkZ_NE || lookup.WalkZ_E != baker.WalkZ_E + || lookup.WalkZ_SE != baker.WalkZ_SE || lookup.WalkZ_S != baker.WalkZ_S + || lookup.WalkZ_SW != baker.WalkZ_SW || lookup.WalkZ_W != baker.WalkZ_W + || lookup.WalkZ_NW != baker.WalkZ_NW) { disagreements++; _output.WriteLine($"Z DIFF @ ({x},{y}) cache=({lookup.WalkZ_N},{lookup.WalkZ_NE},{lookup.WalkZ_E},{lookup.WalkZ_SE},{lookup.WalkZ_S},{lookup.WalkZ_SW},{lookup.WalkZ_W},{lookup.WalkZ_NW}) baker=({baker.WalkZ_N},{baker.WalkZ_NE},{baker.WalkZ_E},{baker.WalkZ_SE},{baker.WalkZ_S},{baker.WalkZ_SW},{baker.WalkZ_W},{baker.WalkZ_NW})"); diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepProbeParityTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepProbeParityTests.cs index 25b80773d..df8ff5da5 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepProbeParityTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepProbeParityTests.cs @@ -52,7 +52,7 @@ public class StaticWalkabilityParityTests // diagonal walkable iff raw-diagonal AND (left-partner OR right-partner). // (Raw masks are correct per spec; baker omits diagonal logic per design.) var newOk = bakerResult.IsWalkable(dir); - if (newOk && ((d & 1) == 1)) + if (newOk && (d & 1) == 1) { var leftPartner = (Direction)((d - 1) & 7); var rightPartner = (Direction)((d + 1) & 7); diff --git a/Projects/UOContent/Engines/Pathing/BitmapAStarAlgorithm.cs b/Projects/UOContent/Engines/Pathing/BitmapAStarAlgorithm.cs index 564791ed5..db4aa35b8 100644 --- a/Projects/UOContent/Engines/Pathing/BitmapAStarAlgorithm.cs +++ b/Projects/UOContent/Engines/Pathing/BitmapAStarAlgorithm.cs @@ -23,7 +23,7 @@ using Server.Systems.FeatureFlags; using CalcMoves = Server.Movement.Movement; using MoveImpl = Server.Movement.MovementImpl; -namespace Server.PathAlgorithms.BitmapAStar; +namespace Server.PathAlgorithms; /// /// A* pathfinder with a single bitmap-cache lookup per cell expansion. Default walkers @@ -43,7 +43,6 @@ public class BitmapAStarAlgorithm : PathAlgorithm public int z; } - private const int MaxDepth = 300; private const int AreaSize = 38; private const int NodeCount = AreaSize * AreaSize * PlaneCount; @@ -51,38 +50,66 @@ public class BitmapAStarAlgorithm : PathAlgorithm private const int PlaneOffset = 128; private const int PlaneCount = 13; private const int PlaneHeight = 20; - public static readonly PathAlgorithm Instance = new BitmapAStarAlgorithm(); + // Default shared singleton (MaxSearchNodes = 1000, set from config in Configure). Typed + // as the concrete class so Configure can set its instance config; assignable anywhere a + // PathAlgorithm is expected. Specialized variants are just additional instances. + public static readonly BitmapAStarAlgorithm Instance = new(); - private static readonly Direction[] _path = new Direction[AreaSize * AreaSize]; - private static readonly PathNode[] _nodes = new PathNode[NodeCount]; - private static readonly byte[] _nodeStates = new byte[NodeCount]; - private static readonly int[] _successors = new int[8]; - private static readonly PriorityQueue _openQueue = new(); + // Scratch buffers — reused across every Find on THIS instance. Per-instance (not static) + // so independently-configured algorithms don't share state. ~320 KB per instance; create + // specialized instances once (static readonly), never per-call. Safe to reuse per Find + // because the game loop is single-threaded and Find is never re-entered. + private readonly Direction[] _path = new Direction[AreaSize * AreaSize]; + private readonly PathNode[] _nodes = new PathNode[NodeCount]; + private readonly byte[] _nodeStates = new byte[NodeCount]; + private readonly int[] _successors = new int[8]; + private readonly PriorityQueue _openQueue = new(); - private static int _xOffset; - private static int _yOffset; + // A* node-expansion budget: the search bails (returning null) after this many node + // expansions. Benchmarked as near-optimal: above the ~500 needed to solve walled-off + // indoor routes, below the ~1500 window-exhaustion cost ceiling where a failed + // (unreachable) search's worst-case cost spikes for no solving benefit. Successful + // searches terminate on goal-found, so this never touches the common open-terrain case. + // Per-instance so specialized algorithms (e.g. a wider-budget variant for special NPCs) + // can coexist; the shared default lives on Instance and is set from config in Configure. + public int MaxSearchNodes { get; set; } = 1000; + + private int _xOffset; + private int _yOffset; // When set, GetSuccessors delegates to the per-cell slow path on every expansion // (creature has CanFly — Z-jumping is beyond the cache's static-only scope). - private static bool _currentMobileNeedsSlowPath; + private bool _currentMobileNeedsSlowPath; // When set, diagonal corner-cut uses the strict AND-rule (BOTH cardinal partners // must be walkable) instead of the lenient creature OR-rule. Cache still applies — // partner bits live in the same source-cell mask byte. Non-GM players only. - private static bool _currentMobilePlayerStrict; + private bool _currentMobilePlayerStrict; // Capability overlay applied to cache results. Layered each cell: // effective = (walkMask & !cantWalk) | (wetMask & canSwim) // Reset at end of Find. - private static bool _currentMobileCanSwim; - private static bool _currentMobileCantWalk; + private bool _currentMobileCanSwim; + private bool _currentMobileCantWalk; // Dynamic-obstacle pass capability flags (per-mobile, captured in Find). // Mirrors MovementImpl.Check's per-mobile derivations so per-cell items/mobiles // checks can be evaluated without re-deriving. - private static bool _currentMobileIgnoreDoors; - private static bool _currentMobileIgnoreSpellFields; - private static bool _currentMobileIgnoreMovableImpassables; + private bool _currentMobileIgnoreDoors; + private bool _currentMobileIgnoreSpellFields; + private bool _currentMobileIgnoreMovableImpassables; + + public static void Configure() + { + // A* node-expansion budget. Default 1000 is benchmarked near-optimal (see + // MaxSearchNodes). Applied to the shared singleton; specialized instances pass their + // own value. Written back to server.cfg on first boot. Auto-invoked at startup via + // AssemblyHandler.Invoke("Configure"). + Instance.MaxSearchNodes = ServerConfiguration.GetOrUpdateSetting( + "pathfinding.maxSearchNodes", + 1000 + ); + } private Point3D _goal; @@ -133,6 +160,7 @@ public class BitmapAStarAlgorithm : PathAlgorithm _currentMobileIgnoreDoors = false; _currentMobileIgnoreMovableImpassables = false; } + // Mirrors MovementImpl: dead/spectral mobiles also ignore doors. _currentMobileIgnoreDoors |= !m.Alive || m.Body.BodyID == 0x3DB || m.IsDeadBondedPet; _currentMobileIgnoreSpellFields = m is PlayerMobile && map != Map.Felucca; @@ -163,7 +191,7 @@ public class BitmapAStarAlgorithm : PathAlgorithm while (_openQueue.Count > 0) { - if (++depth > MaxDepth) + if (++depth > MaxSearchNodes) { break; } @@ -287,7 +315,7 @@ public class BitmapAStarAlgorithm : PathAlgorithm return null; } - private static int GetIndex(int x, int y, int z) + private int GetIndex(int x, int y, int z) { x -= _xOffset; y -= _yOffset; @@ -304,7 +332,7 @@ public class BitmapAStarAlgorithm : PathAlgorithm /// On cache fallthrough or for non-default walkers, defers to /// for THIS cell only. /// - private static int GetSuccessors(int p, Mobile m, Map map) + private int GetSuccessors(int p, Mobile m, Map map) { var px = p % AreaSize; var py = p / AreaSize % AreaSize; @@ -427,7 +455,7 @@ public class BitmapAStarAlgorithm : PathAlgorithm /// non-Felucca players → ignore spell fields). Mobiles: any other mobile whose Z range /// overlaps and which we can't move over. /// - private static bool IsBlockedByDynamic(Mobile m, Map map, int x, int y, int z) + private bool IsBlockedByDynamic(Mobile m, Map map, int x, int y, int z) { var ourTop = z + PersonHeightConst; @@ -504,7 +532,7 @@ public class BitmapAStarAlgorithm : PathAlgorithm /// CheckMovement validates land/statics/items via MovementImpl; dynamic mobile blocking /// is layered on top because MovementImpl doesn't iterate same-cell mobiles. /// - private static int GetSuccessorsSlowPath(Mobile m, Map map, int px, int py, Point3D p3D, int[] vals) + private int GetSuccessorsSlowPath(Mobile m, Map map, int px, int py, Point3D p3D, int[] vals) { var count = 0; diff --git a/Projects/UOContent/Engines/Pathing/Cache/StepCache.cs b/Projects/UOContent/Engines/Pathing/Cache/StepCache.cs index 2be027477..055e19173 100644 --- a/Projects/UOContent/Engines/Pathing/Cache/StepCache.cs +++ b/Projects/UOContent/Engines/Pathing/Cache/StepCache.cs @@ -460,7 +460,27 @@ public sealed class StepCache if (map == null || map == Map.Internal || x < 0 || y < 0 || x >= map.Width || y >= map.Height) { _fallthroughOffMap++; - return new StepMask(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, CacheHitKind.Fallthrough_OffMap); + return new StepMask( + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + CacheHitKind.Fallthrough_OffMap + ); } var chunkX = x >> 4; @@ -489,7 +509,27 @@ public sealed class StepCache else { _fallthroughNotBuilt++; - return new StepMask(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, CacheHitKind.Fallthrough_NotBuilt); + return new StepMask( + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + CacheHitKind.Fallthrough_NotBuilt + ); } } else @@ -524,7 +564,27 @@ public sealed class StepCache return stratumResult; } _fallthroughMultiZ++; - return new StepMask(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, CacheHitKind.Fallthrough_MultiZ); + return new StepMask( + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + CacheHitKind.Fallthrough_MultiZ + ); } // Source-Z guard: the cache stores one answer per cell baked at SourceZ. @@ -564,7 +624,27 @@ public sealed class StepCache } _fallthroughSourceZMismatch++; - return new StepMask(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, CacheHitKind.Fallthrough_SourceZMismatch); + return new StepMask( + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + CacheHitKind.Fallthrough_SourceZMismatch + ); } switch (hitKindResult) diff --git a/Projects/UOContent/Engines/Pathing/Cache/StepCacheFile.cs b/Projects/UOContent/Engines/Pathing/Cache/StepCacheFile.cs index d737e6b52..85afd68e4 100644 --- a/Projects/UOContent/Engines/Pathing/Cache/StepCacheFile.cs +++ b/Projects/UOContent/Engines/Pathing/Cache/StepCacheFile.cs @@ -234,9 +234,7 @@ internal static class StepCacheFile // chunks add another ~2.5 KB (swim layer) but they're a small fraction of any // map; the writer grows on overflow so under-estimating just causes a few // realloc/copy cycles during the bake — not a correctness issue. - var capacity = HeaderSize - + (BytesPerChunkBase + 256) * (int)chunkCount - + IndexEntryBytes * (int)chunkCount; + var capacity = HeaderSize + (BytesPerChunkBase + 256) * (int)chunkCount + IndexEntryBytes * (int)chunkCount; var buffer = new byte[capacity]; var w = new BufferWriter(buffer, prefixStr: false); diff --git a/Projects/UOContent/Engines/Pathing/MovementPath.cs b/Projects/UOContent/Engines/Pathing/MovementPath.cs index eb5d2f9d4..b5307ad3d 100644 --- a/Projects/UOContent/Engines/Pathing/MovementPath.cs +++ b/Projects/UOContent/Engines/Pathing/MovementPath.cs @@ -4,7 +4,6 @@ using Server.Engines.Pathing; using Server.Engines.Pathing.Cache; using Server.Items; using Server.PathAlgorithms; -using Server.PathAlgorithms.BitmapAStar; using Server.Spells; using Server.Targeting; diff --git a/Projects/UOContent/Engines/Pathing/PathDiag.cs b/Projects/UOContent/Engines/Pathing/PathDiag.cs index 51b1372b8..d115c3c6c 100644 --- a/Projects/UOContent/Engines/Pathing/PathDiag.cs +++ b/Projects/UOContent/Engines/Pathing/PathDiag.cs @@ -2,7 +2,7 @@ using System; using System.Diagnostics; using System.IO; using Server.Engines.Pathing.Cache; -using Server.PathAlgorithms.BitmapAStar; +using Server.PathAlgorithms; using Server.Targeting; namespace Server.Engines.Pathing; diff --git a/Projects/UOContent/Engines/Pathing/PathfindRecorder.cs b/Projects/UOContent/Engines/Pathing/PathfindRecorder.cs index 795719911..f21637dc4 100644 --- a/Projects/UOContent/Engines/Pathing/PathfindRecorder.cs +++ b/Projects/UOContent/Engines/Pathing/PathfindRecorder.cs @@ -31,18 +31,15 @@ public static class PathfindRecorder { private static readonly ILogger logger = LogFactory.GetLogger(typeof(PathfindRecorder)); - private static bool _enabled; - private static string _outputPath; private static StreamWriter _writer; - private static long _recordsWritten; - public static bool Enabled => _enabled; - public static string OutputPath => _outputPath; - public static long RecordsWritten => _recordsWritten; + public static bool Enabled { get; private set; } + public static string OutputPath { get; set; } + public static long RecordsWritten { get; private set; } public static void Configure() { - _outputPath = ServerConfiguration.GetOrUpdateSetting( + OutputPath = ServerConfiguration.GetOrUpdateSetting( "pathfinding.recorder.path", Path.Combine(Core.BaseDirectory, "Data", "Pathfinding", "recordings", "pathfinds.jsonl") ); @@ -61,7 +58,7 @@ public static class PathfindRecorder /// public static void SetEnabled(bool enabled) { - if (enabled == _enabled) + if (enabled == Enabled) { return; } @@ -70,22 +67,22 @@ public static class PathfindRecorder { try { - Directory.CreateDirectory(Path.GetDirectoryName(_outputPath) ?? "."); - var stream = new FileStream(_outputPath, FileMode.Append, FileAccess.Write, FileShare.Read); + Directory.CreateDirectory(Path.GetDirectoryName(OutputPath) ?? "."); + var stream = new FileStream(OutputPath, FileMode.Append, FileAccess.Write, FileShare.Read); _writer = new StreamWriter(stream, new UTF8Encoding(false)); - _enabled = true; - logger.Information("PathfindRecorder enabled, writing to {Path}", _outputPath); + Enabled = true; + logger.Information("PathfindRecorder enabled, writing to {Path}", OutputPath); } catch (IOException ex) { - logger.Warning(ex, "PathfindRecorder: failed to open {Path} for write", _outputPath); + logger.Warning(ex, "PathfindRecorder: failed to open {Path} for write", OutputPath); _writer = null; - _enabled = false; + Enabled = false; } } else { - _enabled = false; + Enabled = false; try { _writer?.Flush(); @@ -93,10 +90,10 @@ public static class PathfindRecorder } catch (IOException ex) { - logger.Warning(ex, "PathfindRecorder: error closing {Path}", _outputPath); + logger.Warning(ex, "PathfindRecorder: error closing {Path}", OutputPath); } _writer = null; - logger.Information("PathfindRecorder disabled ({Count} records this session)", _recordsWritten); + logger.Information("PathfindRecorder disabled ({Count} records this session)", RecordsWritten); } } @@ -113,7 +110,7 @@ public static class PathfindRecorder } catch (IOException ex) { - logger.Warning(ex, "PathfindRecorder: flush failed for {Path}", _outputPath); + logger.Warning(ex, "PathfindRecorder: flush failed for {Path}", OutputPath); } } @@ -124,7 +121,7 @@ public static class PathfindRecorder /// public static void RecordIfEnabled(Mobile m, Map map, Point3D start, Point3D goal) { - if (!_enabled || _writer == null || m == null || map == null) + if (!Enabled || _writer == null || m == null || map == null) { return; } @@ -159,7 +156,7 @@ public static class PathfindRecorder vsb.Append(canMoveOverObstacles ? "true" : "false"); vsb.Append("}\n"); _writer.Write(vsb.AsSpan()); - _recordsWritten++; + RecordsWritten++; } catch (IOException ex) {