feat(pathfinding): .swb format v8 compact index (#3b) (#2471)
## Summary Phase #3b (final roadmap item), stacked on #2470. Compacts the index trailer from 20 to 8 bytes/chunk. Trammel: 19.2 MB → 17.9 MB. Roadmap total: 565 MB → 17.9 MB (−96.8%). ## Details - Trailer stores `{ u32 packedKey = (ChunkX << 16) | ChunkY, u32 recordLength }` per chunk, in record write order; the file offset is dropped and reconstructed by cumulative recordLength from HeaderSize. - No record reordering, no varint; fixed-stride, TryReadChunk unchanged. - Also simplifies the accumulated `.swb` code comments across the stack. - Format v8; v7 files rejected and re-baked once. ## Tests v8 multi-chunk round-trip (cumulative offset reconstruction) + the v6/v7 suite; full pathfinding suite green; Release build clean.
This commit is contained in:
parent
c265bcbb5e
commit
c7aaf33de9
16 changed files with 960 additions and 868 deletions
|
|
@ -28,8 +28,6 @@ public static class Deflate
|
|||
|
||||
public static LibDeflateBinding Standard => _standard ??= new LibDeflateBinding();
|
||||
|
||||
// Best-ratio compressor. Construction allocates a native libdeflate compressor, so it is
|
||||
// cached per thread like Standard and reused. Decompression is level-independent, so the
|
||||
// decompress path can use either accessor. libdeflate is not thread-safe — hence ThreadStatic.
|
||||
// Best-ratio compressor, cached per thread like Standard (construction allocates native state).
|
||||
public static LibDeflateBinding Maximum => _maximum ??= new LibDeflateBinding(LibDeflateCompressionLevel.VeryHigh);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,7 @@ namespace Server.Tests.Pathfinding;
|
|||
|
||||
// v7 = per-chunk libdeflate compression on top of the v6 predictive-Z format. Each record is
|
||||
// compressed independently (random access preserved) behind a u32 uncompressed-length prefix;
|
||||
// records that do not shrink (tiny Uniform records) are stored raw. StepCacheFileV6Tests already
|
||||
// round-trips every chunk shape through this compression path; these tests cover the v7-specific
|
||||
// behavior: that compression engages, that the stored-raw fallback round-trips, and v6 rejection.
|
||||
// records that do not shrink (tiny Uniform records) are stored raw.
|
||||
[Collection("Sequential Pathfinding Tests")]
|
||||
public class StepCacheFileV7Tests
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Server.Engines.Pathing.Cache;
|
||||
using Xunit;
|
||||
|
||||
namespace Server.Tests.Pathfinding;
|
||||
|
||||
// v8 = compact index on top of the v7 compression format. The trailer drops the per-chunk file
|
||||
// offset (reconstructed by cumulative record length in write order) and packs the key to u32.
|
||||
[Collection("Sequential Pathfinding Tests")]
|
||||
public class StepCacheFileV8Tests
|
||||
{
|
||||
private static StepChunk VariedChunk(int seed, int multis)
|
||||
{
|
||||
var c = new StepChunk { BuiltMultisVersion = multis };
|
||||
for (var i = 0; i < StepChunk.CellsPerChunk; i++)
|
||||
{
|
||||
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));
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
private static StepChunk UniformChunk(sbyte z, int multis)
|
||||
{
|
||||
var c = new StepChunk { BuiltMultisVersion = multis };
|
||||
Array.Fill(c.WalkMask, (byte)0xC1);
|
||||
Array.Fill(c.SourceZ, z);
|
||||
foreach (var arr in new[]
|
||||
{
|
||||
c.WalkZN, c.WalkZNE, c.WalkZE, c.WalkZSE, c.WalkZS, c.WalkZSW, c.WalkZW, c.WalkZNW,
|
||||
c.SwimZN, c.SwimZNE, c.SwimZE, c.SwimZSE, c.SwimZS, c.SwimZSW, c.SwimZW, c.SwimZNW
|
||||
})
|
||||
{
|
||||
Array.Fill(arr, z);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
private static void AssertBaseEqual(StepChunk a, StepChunk b)
|
||||
{
|
||||
Assert.Equal(a.BuiltMultisVersion, b.BuiltMultisVersion);
|
||||
Assert.True(a.WalkMask.AsSpan().SequenceEqual(b.WalkMask));
|
||||
Assert.True(a.WetMask.AsSpan().SequenceEqual(b.WetMask));
|
||||
Assert.True(a.SourceZ.AsSpan().SequenceEqual(b.SourceZ));
|
||||
var az = new[] { a.WalkZN, a.WalkZNE, a.WalkZE, a.WalkZSE, a.WalkZS, a.WalkZSW, a.WalkZW, a.WalkZNW,
|
||||
a.SwimZN, a.SwimZNE, a.SwimZE, a.SwimZSE, a.SwimZS, a.SwimZSW, a.SwimZW, a.SwimZNW };
|
||||
var bz = new[] { b.WalkZN, b.WalkZNE, b.WalkZE, b.WalkZSE, b.WalkZS, b.WalkZSW, b.WalkZW, b.WalkZNW,
|
||||
b.SwimZN, b.SwimZNE, b.SwimZE, b.SwimZSE, b.SwimZS, b.SwimZSW, b.SwimZW, b.SwimZNW };
|
||||
for (var i = 0; i < az.Length; i++)
|
||||
{
|
||||
Assert.True(az[i].AsSpan().SequenceEqual(bz[i]), $"base Z array {i} differs");
|
||||
}
|
||||
}
|
||||
|
||||
private static string WriteMany(IReadOnlyList<(int cx, int cy, StepChunk c)> chunks)
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"swbv8_{Guid.NewGuid():N}.swb");
|
||||
var idx = 0;
|
||||
StepCacheFile.Write(path, 1u, (uint)chunks.Count, (out int ox, out int oy, out StepChunk oc) =>
|
||||
{
|
||||
if (idx >= chunks.Count) { ox = oy = 0; oc = null!; return false; }
|
||||
var e = chunks[idx++];
|
||||
ox = e.cx; oy = e.cy; oc = e.c;
|
||||
return true;
|
||||
});
|
||||
return path;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultiChunk_RoundTrips_WithDerivedOffsets()
|
||||
{
|
||||
// Distinct chunks at distinct coords. A wrong derived offset would read another chunk's
|
||||
// bytes, so per-chunk identity verifies cumulative offset reconstruction across records.
|
||||
var chunks = new List<(int, int, StepChunk)>
|
||||
{
|
||||
(1, 1, VariedChunk(seed: 3, multis: 2)),
|
||||
(2, 5, UniformChunk(z: 14, multis: 9)), // tiny record (stored-raw path) in the middle
|
||||
(10, 3, VariedChunk(seed: 99, multis: 4)),
|
||||
(300, 200, VariedChunk(seed: 17, multis: 5)), // large packed-key coords (high 16 bits)
|
||||
};
|
||||
|
||||
var path = WriteMany(chunks);
|
||||
try
|
||||
{
|
||||
using var reader = StepCacheFile.OpenForLazy(path);
|
||||
Assert.NotNull(reader);
|
||||
Assert.Equal((uint)chunks.Count, reader!.ChunkCount);
|
||||
|
||||
foreach (var (cx, cy, src) in chunks)
|
||||
{
|
||||
Assert.True(reader.Has(cx, cy), $"missing chunk ({cx},{cy})");
|
||||
var rt = reader.TryReadChunk(cx, cy);
|
||||
Assert.NotNull(rt);
|
||||
AssertBaseEqual(src, rt!);
|
||||
}
|
||||
|
||||
// A coordinate that was never written must not resolve.
|
||||
Assert.Null(reader.TryReadChunk(7, 7));
|
||||
}
|
||||
finally { File.Delete(path); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void V7_IsRejected()
|
||||
{
|
||||
var path = WriteMany(new List<(int, int, StepChunk)> { (0, 0, UniformChunk(z: 10, multis: 1)) });
|
||||
try
|
||||
{
|
||||
var bytes = File.ReadAllBytes(path);
|
||||
bytes[4] = 7; bytes[5] = 0; bytes[6] = 0; bytes[7] = 0; // version 7 < MinSupportedVersion 8
|
||||
File.WriteAllBytes(path, bytes);
|
||||
Assert.Null(StepCacheFile.OpenForLazy(path));
|
||||
}
|
||||
finally { File.Delete(path); }
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Server.Engines.Pathing;
|
||||
using Server.Engines.Pathing.Cache;
|
||||
using Server.Mobiles;
|
||||
using Server.Systems.FeatureFlags;
|
||||
|
|
@ -114,7 +115,7 @@ public class BitmapAStarAlgorithm : PathAlgorithm
|
|||
// and trips the threshold immediately.
|
||||
StepCache.Instance.BeginFindGeneration();
|
||||
|
||||
Server.Engines.Pathing.PathfindRecorder.RecordIfEnabled(m, map, start, goal);
|
||||
PathfindRecorder.RecordIfEnabled(m, map, start, goal);
|
||||
|
||||
_currentMobileNeedsSlowPath = RequiresSlowPath(m);
|
||||
_currentMobilePlayerStrict = m.Player && m.AccessLevel < AccessLevel.GameMaster;
|
||||
|
|
@ -548,6 +549,5 @@ public class BitmapAStarAlgorithm : PathAlgorithm
|
|||
/// + wetMask). CanOpenDoors / CanMoveOverObstacles only affect dynamic items and don't
|
||||
/// disqualify the cache.
|
||||
/// </summary>
|
||||
private static bool RequiresSlowPath(Mobile m) =>
|
||||
m is BaseCreature bc && bc.CanFly;
|
||||
private static bool RequiresSlowPath(Mobile m) => m is BaseCreature bc && bc.CanFly;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ public class CacheEvictionTimer : Timer
|
|||
_instance.Start();
|
||||
}
|
||||
|
||||
private CacheEvictionTimer() : base(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60)) { }
|
||||
private CacheEvictionTimer() : base(TimeSpan.FromSeconds(120), TimeSpan.FromSeconds(120)) { }
|
||||
|
||||
protected override void OnTick()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ public sealed class StepCache
|
|||
private readonly Dictionary<long, StepChunk> _chunks = new();
|
||||
// Parallel list of keys for O(1) random sampling during eviction. Kept in lockstep
|
||||
// with _chunks: append on Miss_NotBuilt, swap-and-pop on eviction.
|
||||
private readonly List<long> _keysList = new();
|
||||
private readonly List<long> _keysList = [];
|
||||
|
||||
// Second-touch promotion tracker. A chunk's first miss within the window returns
|
||||
// Fallthrough_NotBuilt; the caller takes the slow path. The Nth DISTINCT-FIND miss
|
||||
|
|
@ -38,7 +38,6 @@ public sealed class StepCache
|
|||
// Generation counter incremented by BeginFindGeneration(). Sentinel 0 = "no Find started
|
||||
// yet"; treated as a distinct generation per call so callers that bypass BeginFindGeneration
|
||||
// (single-call tests, BakeMap with threshold=1) get sensible behavior.
|
||||
private uint _findGeneration;
|
||||
|
||||
private struct ChunkMissState
|
||||
{
|
||||
|
|
@ -93,12 +92,16 @@ public sealed class StepCache
|
|||
/// </summary>
|
||||
public void BeginFindGeneration()
|
||||
{
|
||||
unchecked { _findGeneration++; }
|
||||
if (_findGeneration == 0) { _findGeneration = 1; }
|
||||
unchecked { CurrentFindGeneration++; }
|
||||
|
||||
if (CurrentFindGeneration == 0)
|
||||
{
|
||||
CurrentFindGeneration = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Test-only: read the current Find generation.</summary>
|
||||
internal uint CurrentFindGeneration => _findGeneration;
|
||||
internal uint CurrentFindGeneration { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Pack (mapId, chunkX, chunkY) into a single long key.
|
||||
|
|
@ -107,7 +110,7 @@ public sealed class StepCache
|
|||
internal static long EncodeKey(int mapId, int chunkX, int chunkY) =>
|
||||
((long)(mapId & 0xFFFF) << 32) | ((long)(chunkX & 0xFFFF) << 16) | (long)(chunkY & 0xFFFF);
|
||||
|
||||
public CacheStats GetStats() => new CacheStats(
|
||||
public CacheStats GetStats() => new(
|
||||
residentChunks: _chunks.Count,
|
||||
hits: _hits,
|
||||
missesNotBuilt: _missesNotBuilt,
|
||||
|
|
@ -142,7 +145,7 @@ public sealed class StepCache
|
|||
_chunks.Clear();
|
||||
_keysList.Clear();
|
||||
_chunkMissTracker.Clear();
|
||||
_findGeneration = 0;
|
||||
CurrentFindGeneration = 0;
|
||||
_hits = 0;
|
||||
_missesNotBuilt = 0;
|
||||
_missesDirtyRebuild = 0;
|
||||
|
|
@ -193,10 +196,17 @@ public sealed class StepCache
|
|||
// and the bake would write an empty file. Force eager build for the duration.
|
||||
var prevThreshold = MissPromotionThreshold;
|
||||
MissPromotionThreshold = 1;
|
||||
var startTick = Core.TickCount;
|
||||
try
|
||||
{
|
||||
var chunkCols = (map.Width + ChunkSize - 1) / ChunkSize;
|
||||
var chunkRows = (map.Height + ChunkSize - 1) / ChunkSize;
|
||||
var logEvery = Math.Max(1, chunkRows / 32);
|
||||
|
||||
logger.Information(
|
||||
"PathBake map {MapId}: walking {Cols}x{Rows} = {Total} chunks (synchronous; no eviction during the walk)...",
|
||||
mapId, chunkCols, chunkRows, chunkCols * chunkRows
|
||||
);
|
||||
|
||||
for (var cy = 0; cy < chunkRows; cy++)
|
||||
{
|
||||
|
|
@ -206,7 +216,21 @@ public sealed class StepCache
|
|||
// whether the query returns Hit or Fallthrough_SourceZMismatch.
|
||||
TryGetMask(map, cx * ChunkSize, cy * ChunkSize, sourceZ: 0);
|
||||
}
|
||||
|
||||
if ((cy + 1) % logEvery == 0 || cy == chunkRows - 1)
|
||||
{
|
||||
logger.Information(
|
||||
"PathBake map {MapId}: row {Row}/{Rows} ({Pct}%), {Resident} chunks resident, {Elapsed:F1}s, {HeapMB} MB heap",
|
||||
mapId, cy + 1, chunkRows, (cy + 1) * 100 / chunkRows,
|
||||
_chunks.Count, (Core.TickCount - startTick) / 1000.0, GC.GetTotalMemory(false) >> 20
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
logger.Information(
|
||||
"PathBake map {MapId}: walk complete in {Elapsed:F1}s, writing {Resident} chunks to disk...",
|
||||
mapId, (Core.TickCount - startTick) / 1000.0, _chunks.Count
|
||||
);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -610,7 +634,7 @@ public sealed class StepCache
|
|||
// Environment.TickCount, not Core.TickCount: tests/bench fixtures may not advance
|
||||
// the game-loop tick. The promotion window is wall-clock anyway.
|
||||
var now = (uint)Environment.TickCount;
|
||||
var gen = _findGeneration;
|
||||
var gen = CurrentFindGeneration;
|
||||
|
||||
if (_chunkMissTracker.TryGetValue(chunkKey, out var state))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@ namespace Server.Engines.Pathing.Cache;
|
|||
/// only when the cache asks for them. RAM stays bounded by MaxResidentChunks regardless
|
||||
/// of file size.
|
||||
///
|
||||
/// File layout v7 (little-endian, BufferWriter / BufferReader convention):
|
||||
/// File layout v8 (little-endian, BufferWriter / BufferReader convention):
|
||||
///
|
||||
/// Header (48 bytes):
|
||||
/// Header (40 bytes):
|
||||
/// u32 Magic = 0x42575300 ('SWB\0')
|
||||
/// u32 Version = current FormatVersion (7)
|
||||
/// u32 Version = current FormatVersion (8)
|
||||
/// u32 MapId
|
||||
/// u64 Fingerprint XxHash3 over (1) LandTable + ItemTable flags AND (2) the
|
||||
/// on-disk bytes of mapX.mul / .uop, staidxX.mul, staticsX.mul.
|
||||
|
|
@ -73,8 +73,10 @@ namespace Server.Engines.Pathing.Cache;
|
|||
/// sbyte walkZ_N..NW (8)
|
||||
/// sbyte swimZ_N..NW (8)
|
||||
///
|
||||
/// Index trailer (20 × ChunkCount bytes):
|
||||
/// For each chunk: { u64 chunkKey, u64 fileOffset, u32 recordLength }
|
||||
/// Index trailer (8 × ChunkCount bytes), in record write order:
|
||||
/// For each chunk: { u32 packedKey = (ChunkX << 16) | ChunkY, u32 recordLength }
|
||||
/// The file offset is not stored — reconstructed as a cumulative sum of recordLength
|
||||
/// starting at HeaderSize (the first record sits immediately after the header).
|
||||
///
|
||||
/// Per-chunk fixed portion (Kind + flags + ZArrayMask + WalkMask + WetMask + SourceZ):
|
||||
/// ~783 bytes; each present base Z array adds 256 bytes (0..16 present, so up to ~4 KB).
|
||||
|
|
@ -88,29 +90,14 @@ namespace Server.Engines.Pathing.Cache;
|
|||
internal static class StepCacheFile
|
||||
{
|
||||
public const uint Magic = 0x42575300; // 'SWB\0'
|
||||
public const uint FormatVersion = 7;
|
||||
public const uint FormatVersion = 8;
|
||||
|
||||
/// <summary>
|
||||
/// Lowest format version this binary can load. Files below this version are treated as
|
||||
/// missing (silently rejected) — a subsequent SaveToFile / BakeMap overwrites them with
|
||||
/// the current FormatVersion. Bumped to 3 when the swim layer landed (v2 had no swim
|
||||
/// layer). Bumped to 4 when the baker switched to clearance-aware standable-surface
|
||||
/// strata: v3 bakes anchored every cell at the land average and so missed walkable
|
||||
/// static-over-land surfaces (sewer/dungeon walkways, bridges, upper building floors),
|
||||
/// producing ~98% source-Z fallthroughs on those routes. The on-disk layout is
|
||||
/// unchanged; only the strata population differs, so the bump exists purely to force a
|
||||
/// one-time re-bake of stale v3 files on first boot under the new binary. Bumped to 5 for
|
||||
/// uniform-chunk elision: each record now begins with a Kind byte (0 = Full, 2 = Uniform);
|
||||
/// a fully-uniform chunk (no strata, no swim layer, all 19 base arrays constant) stores
|
||||
/// one cell's worth of data (~28 bytes) instead of the full record. Bumped to 6 for
|
||||
/// predictive-Z residuals: each base directional Z array is stored as a masked residual
|
||||
/// against SourceZ (a ZArrayMask u16 flags which arrays are present); arrays matching their
|
||||
/// prediction are omitted and synthesized at read. v5 files are rejected and re-baked once.
|
||||
/// Bumped to 7 for per-chunk compression: each chunk record is libdeflate-compressed
|
||||
/// independently (random access preserved) behind a u32 uncompressed-length prefix; tiny
|
||||
/// records that do not shrink are stored raw. v6 files are rejected and re-baked once.
|
||||
/// Lowest format version this binary can load. Files below it are treated as missing
|
||||
/// (silently rejected) and overwritten by the next SaveToFile / BakeMap. The cache is
|
||||
/// fully regenerable, so a format bump just forces a one-time re-bake of stale files.
|
||||
/// </summary>
|
||||
public const uint MinSupportedVersion = 7;
|
||||
public const uint MinSupportedVersion = 8;
|
||||
|
||||
// Per-chunk record discriminator (first byte after BuiltMultisVersion). 1 is reserved.
|
||||
private const byte KindFull = 0;
|
||||
|
|
@ -125,10 +112,10 @@ internal static class StepCacheFile
|
|||
+ sizeof(uint) // ChunkCount
|
||||
+ sizeof(ulong); // IndexOffset
|
||||
|
||||
// Index entry: chunkKey + fileOffset + recordLength. Bumped to include length when
|
||||
// strata made chunk records variable-size; the lazy reader uses length to do a
|
||||
// single bulk read per chunk without consulting the next offset.
|
||||
private const int IndexEntryBytes = sizeof(ulong) + sizeof(ulong) + sizeof(uint);
|
||||
// Index entry (v8 compact): u32 packedKey ((chunkX << 16) | chunkY) + u32 recordLength.
|
||||
// The file offset is NOT stored — entries are in record write order, so the reader
|
||||
// reconstructs each offset by cumulative sum of record lengths starting at HeaderSize.
|
||||
private const int IndexEntryBytes = sizeof(uint) + sizeof(uint);
|
||||
|
||||
/// <summary>Fixed-size portion of a chunk record (everything except the optional strata + swim trailers).</summary>
|
||||
private const int BytesPerChunkBase =
|
||||
|
|
@ -141,12 +128,6 @@ internal static class StepCacheFile
|
|||
+ 8 * StepChunk.CellsPerChunk // WalkZ[8]
|
||||
+ 8 * StepChunk.CellsPerChunk; // SwimZ[8]
|
||||
|
||||
/// <summary>Swim-layer trailer overhead when present: per-cell SourceZ + Mask + 8×Z arrays.</summary>
|
||||
private const int SwimLayerOverhead = 10 * StepChunk.CellsPerChunk;
|
||||
|
||||
/// <summary>Strata trailer overhead when present: 256×u16 offset table + u32 data length.</summary>
|
||||
private const int StrataTrailerOverhead = StepChunk.CellsPerChunk * sizeof(ushort) + sizeof(uint);
|
||||
|
||||
/// <summary>
|
||||
/// Byte offset of the IndexOffset u64 within the header
|
||||
/// (Magic+Version+MapId+Fingerprint+BakeTimestamp+ChunkCount = 32). Patched after chunks land.
|
||||
|
|
@ -267,11 +248,8 @@ internal static class StepCacheFile
|
|||
w.Write(chunkCount);
|
||||
w.Write(0UL); // IndexOffset placeholder, patched after chunks
|
||||
|
||||
// Per-chunk compression: each record is built uncompressed into `recordScratch`, then
|
||||
// libdeflate-compressed into `compScratch` and framed as [u32 uncompressedLen][payload].
|
||||
// Reuse the cached per-thread VeryHigh compressor (construction allocates native state).
|
||||
// libdeflate is not thread-safe, but bake runs single-threaded. VeryHigh is the best-ratio
|
||||
// level and its slow compression is irrelevant offline (decompression is what's hot, ~1.8us).
|
||||
// Each record is built uncompressed into recordScratch, then libdeflate-compressed into
|
||||
// compScratch and framed as [u32 uncompressedLen][payload].
|
||||
var packer = Deflate.Maximum;
|
||||
var recordScratch = new byte[BytesPerChunkBase + 1024];
|
||||
var compScratch = new byte[packer.MaxPackSize(recordScratch.Length)];
|
||||
|
|
@ -303,8 +281,11 @@ internal static class StepCacheFile
|
|||
var indexOffset = (ulong)w.Position;
|
||||
for (var i = 0u; i < chunkCount; i++)
|
||||
{
|
||||
w.Write(indexEntries[i].key);
|
||||
w.Write(indexEntries[i].offset);
|
||||
// v8 compact entry: u32 packedKey ((chunkX << 16) | chunkY) + u32 recordLength.
|
||||
// Offset is omitted; entries are in record write order so the reader derives it.
|
||||
var key = indexEntries[i].key;
|
||||
var packedKey = ((uint)(key >> 32) << 16) | (uint)(key & 0xFFFF);
|
||||
w.Write(packedKey);
|
||||
w.Write(indexEntries[i].length);
|
||||
}
|
||||
|
||||
|
|
@ -383,14 +364,19 @@ internal static class StepCacheFile
|
|||
return null;
|
||||
}
|
||||
|
||||
// v8 compact index: { u32 packedKey, u32 length } per chunk, in record write order.
|
||||
// The file offset is not stored — reconstruct it by cumulative record length starting
|
||||
// at the first record (immediately after the header).
|
||||
var offsets = new Dictionary<ulong, (ulong offset, uint length)>((int)chunkCount);
|
||||
var runningOffset = (ulong)HeaderSize;
|
||||
for (var i = 0; i < chunkCount; i++)
|
||||
{
|
||||
var entry = indexBuf.AsSpan(i * IndexEntryBytes);
|
||||
var key = BinaryPrimitives.ReadUInt64LittleEndian(entry);
|
||||
var off = BinaryPrimitives.ReadUInt64LittleEndian(entry[8..]);
|
||||
var len = BinaryPrimitives.ReadUInt32LittleEndian(entry[16..]);
|
||||
offsets[key] = (off, len);
|
||||
var packedKey = BinaryPrimitives.ReadUInt32LittleEndian(entry);
|
||||
var len = BinaryPrimitives.ReadUInt32LittleEndian(entry[4..]);
|
||||
var key = PackChunkKey((int)(packedKey >> 16), (int)(packedKey & 0xFFFF));
|
||||
offsets[key] = (runningOffset, len);
|
||||
runningOffset += len;
|
||||
}
|
||||
|
||||
return new LazyReader(stream, mapId, fingerprint, bakeTimestamp, chunkCount, offsets);
|
||||
|
|
@ -423,14 +409,17 @@ internal static class StepCacheFile
|
|||
internal static sbyte DecodeZ(sbyte predict, sbyte residual) => unchecked((sbyte)(predict + residual));
|
||||
|
||||
/// <summary>
|
||||
/// The 16 base directional-Z arrays in canonical order: walk N..NW (indices 0-7),
|
||||
/// then swim N..NW (8-15). Index d uses WalkMask (d < 8) or WetMask (d >= 8)
|
||||
/// with direction bit (d & 7). Allocates a 16-slot reference array (bake/read time only).
|
||||
/// The base directional-Z array for direction index d in canonical order: walk N..NW (0-7),
|
||||
/// then swim N..NW (8-15). Index d uses WalkMask (d < 8) or WetMask (d >= 8) with
|
||||
/// direction bit (d & 7).
|
||||
/// </summary>
|
||||
private static sbyte[][] GetBaseZArrays(StepChunk c) => new[]
|
||||
private static sbyte[] GetBaseZArray(StepChunk c, int d) => d switch
|
||||
{
|
||||
c.WalkZN, c.WalkZNE, c.WalkZE, c.WalkZSE, c.WalkZS, c.WalkZSW, c.WalkZW, c.WalkZNW,
|
||||
c.SwimZN, c.SwimZNE, c.SwimZE, c.SwimZSE, c.SwimZS, c.SwimZSW, c.SwimZW, c.SwimZNW,
|
||||
0 => c.WalkZN, 1 => c.WalkZNE, 2 => c.WalkZE, 3 => c.WalkZSE,
|
||||
4 => c.WalkZS, 5 => c.WalkZSW, 6 => c.WalkZW, 7 => c.WalkZNW,
|
||||
8 => c.SwimZN, 9 => c.SwimZNE, 10 => c.SwimZE, 11 => c.SwimZSE,
|
||||
12 => c.SwimZS, 13 => c.SwimZSW, 14 => c.SwimZW, 15 => c.SwimZNW,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(d))
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -515,11 +504,10 @@ internal static class StepCacheFile
|
|||
// Predictive-Z: each base directional Z array is stored as a masked residual against
|
||||
// SourceZ. Bit d of ZArrayMask is set only when array d differs from its prediction
|
||||
// somewhere; cleared arrays are omitted and rebuilt from mask+SourceZ at read.
|
||||
var zArrays = GetBaseZArrays(chunk);
|
||||
ushort zArrayMask = 0;
|
||||
for (var d = 0; d < 16; d++)
|
||||
{
|
||||
var z = zArrays[d];
|
||||
var z = GetBaseZArray(chunk, d);
|
||||
var dirMask = d < 8 ? chunk.WalkMask : chunk.WetMask;
|
||||
var bit = d & 7;
|
||||
for (var cell = 0; cell < StepChunk.CellsPerChunk; cell++)
|
||||
|
|
@ -544,7 +532,7 @@ internal static class StepCacheFile
|
|||
{
|
||||
continue;
|
||||
}
|
||||
var z = zArrays[d];
|
||||
var z = GetBaseZArray(chunk, d);
|
||||
var dirMask = d < 8 ? chunk.WalkMask : chunk.WetMask;
|
||||
var bit = d & 7;
|
||||
for (var cell = 0; cell < StepChunk.CellsPerChunk; cell++)
|
||||
|
|
@ -629,11 +617,10 @@ internal static class StepCacheFile
|
|||
|
||||
// Predictive-Z reconstruction: present arrays carry residuals (z = predict + residual);
|
||||
// absent arrays are synthesized from mask+SourceZ (z = predict, residual implicitly 0).
|
||||
var zArrays = GetBaseZArrays(chunk);
|
||||
Span<sbyte> residual = stackalloc sbyte[StepChunk.CellsPerChunk];
|
||||
for (var d = 0; d < 16; d++)
|
||||
{
|
||||
var z = zArrays[d];
|
||||
var z = GetBaseZArray(chunk, d);
|
||||
var dirMask = d < 8 ? chunk.WalkMask : chunk.WetMask;
|
||||
var bit = d & 7;
|
||||
if ((zArrayMask >> d & 1) != 0)
|
||||
|
|
@ -787,9 +774,7 @@ internal static class StepCacheFile
|
|||
}
|
||||
else
|
||||
{
|
||||
// Decompression is level-independent, so reuse the shared per-thread binding
|
||||
// rather than allocating a native decompressor per reader. The cache is read on
|
||||
// the single game thread; libdeflate's non-thread-safety is satisfied by ThreadStatic.
|
||||
// Decompression is level-independent, so reuse the shared per-thread binding.
|
||||
var result = Deflate.Standard.Unpack(
|
||||
_bodyBuffer.AsSpan(0, uncompressedLen),
|
||||
_buffer.AsSpan(sizeof(uint), payloadLen),
|
||||
|
|
|
|||
|
|
@ -4,17 +4,19 @@ namespace Server.Engines.Pathing.Cache;
|
|||
|
||||
/// <summary>
|
||||
/// Per-chunk storage backing StepCache. Holds raw walk + swim masks and destination Z
|
||||
/// values for each of 256 cells in a 16x16 chunk, plus build-time metadata (multis
|
||||
/// version, multi-Z strata) and LRU bookkeeping.
|
||||
/// values for each of 256 cells in a 16x16 chunk, plus build-time metadata (multis version, multi-Z strata)
|
||||
/// and LRU bookkeeping.
|
||||
/// </summary>
|
||||
internal sealed class StepChunk
|
||||
{
|
||||
public const int CellsPerChunk = 256; // 16 x 16
|
||||
|
||||
/// <summary>Bit i of WalkMask[c] = "default walker can step from cell c to neighbor (Direction)i". Raw — no diagonal corner-cut applied here.</summary>
|
||||
/// <summary>Bit i of WalkMask[c] = "default walker can step from cell c to neighbor (Direction)i".
|
||||
/// Raw — no diagonal corner-cut applied here.</summary>
|
||||
public readonly byte[] WalkMask = new byte[CellsPerChunk];
|
||||
|
||||
/// <summary>Bit i of WetMask[c] = "swim-only mob can step from cell c to neighbor (Direction)i". Layered with WalkMask via canSwim/cantWalk capability flags.</summary>
|
||||
/// <summary>Bit i of WetMask[c] = "swim-only mob can step from cell c to neighbor (Direction)i".
|
||||
/// Layered with WalkMask via canSwim/cantWalk capability flags.</summary>
|
||||
public readonly byte[] WetMask = new byte[CellsPerChunk];
|
||||
|
||||
public readonly sbyte[] SourceZ = new sbyte[CellsPerChunk];
|
||||
|
|
@ -103,25 +105,6 @@ internal sealed class StepChunk
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>Test/serialization hook: install pre-built swim-layer arrays. Pass nulls to clear.</summary>
|
||||
internal void SetSwimLayer(
|
||||
sbyte[] swimSourceZ, byte[] swimMask,
|
||||
sbyte[] zN, sbyte[] zNE, sbyte[] zE, sbyte[] zSE,
|
||||
sbyte[] zS, sbyte[] zSW, sbyte[] zW, sbyte[] zNW
|
||||
)
|
||||
{
|
||||
_swimSourceZ = swimSourceZ;
|
||||
_swimMask = swimMask;
|
||||
_swimZN_extra = zN;
|
||||
_swimZNE_extra = zNE;
|
||||
_swimZE_extra = zE;
|
||||
_swimZSE_extra = zSE;
|
||||
_swimZS_extra = zS;
|
||||
_swimZSW_extra = zSW;
|
||||
_swimZW_extra = zW;
|
||||
_swimZNW_extra = zNW;
|
||||
}
|
||||
|
||||
/// <summary>Sentinel: cell has no strata — single-Z, use the main Walk/Wet arrays.</summary>
|
||||
public const ushort NoStrata = ushort.MaxValue;
|
||||
|
||||
|
|
@ -151,8 +134,7 @@ internal sealed class StepChunk
|
|||
|
||||
public bool IsCellMultiZ(int cellIndex) => GetStrataOffset(cellIndex) != NoStrata;
|
||||
|
||||
public ushort GetStrataOffset(int cellIndex) =>
|
||||
_strataOffsetByCell == null ? NoStrata : _strataOffsetByCell[cellIndex];
|
||||
public ushort GetStrataOffset(int cellIndex) => _strataOffsetByCell == null ? NoStrata : _strataOffsetByCell[cellIndex];
|
||||
|
||||
public ReadOnlySpan<byte> StrataData =>
|
||||
_strataData == null ? ReadOnlySpan<byte>.Empty : _strataData.AsSpan();
|
||||
|
|
@ -182,18 +164,13 @@ internal sealed class StepChunk
|
|||
/// record. Chunks with a swim layer (shore cells) are never uniform — their per-cell swim
|
||||
/// data must be preserved via the Full record.
|
||||
/// </summary>
|
||||
internal bool IsUniform()
|
||||
{
|
||||
if (_strataOffsetByCell != null || HasSwimLayer)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return AllSame(WalkMask) && AllSame(WetMask) && AllSame(SourceZ)
|
||||
&& AllSame(WalkZN) && AllSame(WalkZNE) && AllSame(WalkZE) && AllSame(WalkZSE)
|
||||
&& AllSame(WalkZS) && AllSame(WalkZSW) && AllSame(WalkZW) && AllSame(WalkZNW)
|
||||
&& AllSame(SwimZN) && AllSame(SwimZNE) && AllSame(SwimZE) && AllSame(SwimZSE)
|
||||
&& AllSame(SwimZS) && AllSame(SwimZSW) && AllSame(SwimZW) && AllSame(SwimZNW);
|
||||
}
|
||||
internal bool IsUniform() => _strataOffsetByCell == null
|
||||
&& !HasSwimLayer
|
||||
&& AllSame(WalkMask) && AllSame(WetMask) && AllSame(SourceZ)
|
||||
&& AllSame(WalkZN) && AllSame(WalkZNE) && AllSame(WalkZE) && AllSame(WalkZSE)
|
||||
&& AllSame(WalkZS) && AllSame(WalkZSW) && AllSame(WalkZW) && AllSame(WalkZNW)
|
||||
&& AllSame(SwimZN) && AllSame(SwimZNE) && AllSame(SwimZE) && AllSame(SwimZSE)
|
||||
&& AllSame(SwimZS) && AllSame(SwimZSW) && AllSame(SwimZW) && AllSame(SwimZNW);
|
||||
|
||||
// "All 256 cells equal" via SIMD-accelerated ContainsAnyExcept (skip cell 0, the reference).
|
||||
private static bool AllSame(byte[] a) => a.Length < 2 || !a.AsSpan(1).ContainsAnyExcept(a[0]);
|
||||
|
|
|
|||
|
|
@ -242,8 +242,8 @@ public static class StepProbe
|
|||
/// </summary>
|
||||
public static int ComputeStandingZ(Map map, int x, int y, int locZ)
|
||||
{
|
||||
GetStaticStartZ(map, x, y, locZ, canSwim: false, cantWalk: false,
|
||||
out _, out _, out var zCenter);
|
||||
GetStaticStartZ(map, x, y, locZ, canSwim: false, cantWalk: false, out _, out _, out var zCenter);
|
||||
|
||||
return zCenter;
|
||||
}
|
||||
|
||||
|
|
@ -255,11 +255,7 @@ public static class StepProbe
|
|||
/// </summary>
|
||||
public static int ComputeSwimStandingZ(Map map, int x, int y)
|
||||
{
|
||||
if (map == null || map == Map.Internal)
|
||||
{
|
||||
return int.MinValue;
|
||||
}
|
||||
if (x < 0 || y < 0 || x >= map.Width || y >= map.Height)
|
||||
if (map == null || map == Map.Internal || x < 0 || y < 0 || x >= map.Width || y >= map.Height)
|
||||
{
|
||||
return int.MinValue;
|
||||
}
|
||||
|
|
@ -290,8 +286,7 @@ public static class StepProbe
|
|||
/// Mirrors GetStartZ from MovementImpl, parameterized by canSwim / cantWalk.
|
||||
/// </summary>
|
||||
private static void GetStaticStartZ(
|
||||
Map map, int x, int y, int locZ, bool canSwim, bool cantWalk,
|
||||
out int zLow, out int zTop, out int zCenter
|
||||
Map map, int x, int y, int locZ, bool canSwim, bool cantWalk, out int zLow, out int zTop, out int zCenter
|
||||
)
|
||||
{
|
||||
var landTile = map.Tiles.GetLandTile(x, y);
|
||||
|
|
@ -300,8 +295,7 @@ public static class StepProbe
|
|||
|
||||
// Mirrors MovementImpl: impassable + swim on water is OK; otherwise block on
|
||||
// cantWalk or impassable.
|
||||
var landBlocks = (cantWalk || impassable)
|
||||
&& !(impassable && canSwim && (flags & TileFlag.Wet) != 0);
|
||||
var landBlocks = (cantWalk || impassable) && !(impassable && canSwim && (flags & TileFlag.Wet) != 0);
|
||||
|
||||
map.GetAverageZ(x, y, out var landZ, out var landCenter, out var landTop);
|
||||
|
||||
|
|
@ -356,8 +350,7 @@ public static class StepProbe
|
|||
/// Items and mobile collision phases are omitted.
|
||||
/// </summary>
|
||||
private static bool CheckStaticStep(
|
||||
Map map, int x, int y, int startZ, int startTop, bool canSwim, bool cantWalk,
|
||||
out int newZ
|
||||
Map map, int x, int y, int startZ, int startTop, bool canSwim, bool cantWalk, out int newZ
|
||||
)
|
||||
{
|
||||
newZ = 0;
|
||||
|
|
@ -371,8 +364,7 @@ public static class StepProbe
|
|||
var flags = TileData.LandTable[landTile.ID & TileData.MaxLandValue].Flags;
|
||||
var impassable = (flags & TileFlag.Impassable) != 0;
|
||||
|
||||
var landBlocks = (cantWalk || impassable)
|
||||
&& !(impassable && canSwim && (flags & TileFlag.Wet) != 0);
|
||||
var landBlocks = (cantWalk || impassable) && !(impassable && canSwim && (flags & TileFlag.Wet) != 0);
|
||||
|
||||
var considerLand = !landTile.Ignored;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
namespace Server
|
||||
{
|
||||
public delegate MoveResult MoveMethod(Direction d, bool badStateOk);
|
||||
namespace Server;
|
||||
|
||||
public enum MoveResult
|
||||
{
|
||||
BadState,
|
||||
Blocked,
|
||||
Success,
|
||||
SuccessAutoTurn
|
||||
}
|
||||
}
|
||||
public delegate MoveResult MoveMethod(Direction d, bool badStateOk);
|
||||
|
||||
public enum MoveResult
|
||||
{
|
||||
BadState,
|
||||
Blocked,
|
||||
Success,
|
||||
SuccessAutoTurn
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -8,115 +8,114 @@ using Server.PathAlgorithms.BitmapAStar;
|
|||
using Server.Spells;
|
||||
using Server.Targeting;
|
||||
|
||||
namespace Server
|
||||
namespace Server;
|
||||
|
||||
public sealed class MovementPath
|
||||
{
|
||||
public sealed class MovementPath
|
||||
public MovementPath(Mobile m, Point3D goal)
|
||||
{
|
||||
public MovementPath(Mobile m, Point3D goal)
|
||||
var start = m.Location;
|
||||
var map = m.Map;
|
||||
|
||||
Map = map;
|
||||
Start = start;
|
||||
Goal = goal;
|
||||
|
||||
if (map == null || map == Map.Internal)
|
||||
{
|
||||
var start = m.Location;
|
||||
var map = m.Map;
|
||||
|
||||
Map = map;
|
||||
Start = start;
|
||||
Goal = goal;
|
||||
|
||||
if (map == null || map == Map.Internal)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Utility.InRange(start, goal, 1))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var alg = OverrideAlgorithm ?? BitmapAStarAlgorithm.Instance;
|
||||
|
||||
if (alg?.CheckCondition(m, map, start, goal) == true)
|
||||
{
|
||||
Directions = alg.Find(m, map, start, goal);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine("Warning: {0}: Pathing error from {1} to {2}", e.GetType().Name, start, goal);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
public Map Map { get; }
|
||||
|
||||
public Point3D Start { get; }
|
||||
|
||||
public Point3D Goal { get; }
|
||||
|
||||
public Direction[] Directions { get; }
|
||||
|
||||
public bool Success => Directions?.Length > 0;
|
||||
|
||||
public static PathAlgorithm OverrideAlgorithm { get; set; }
|
||||
|
||||
public static void Configure()
|
||||
if (Utility.InRange(start, goal, 1))
|
||||
{
|
||||
CommandSystem.Register("Path", AccessLevel.GameMaster, Path_OnCommand);
|
||||
CacheEvictionTimer.Configure();
|
||||
PathCacheCommands.Configure();
|
||||
return;
|
||||
}
|
||||
|
||||
[Usage("Path")]
|
||||
[Description("Draws a path from your current location to a targeted location.")]
|
||||
public static void Path_OnCommand(CommandEventArgs e)
|
||||
try
|
||||
{
|
||||
e.Mobile.BeginTarget(-1, true, TargetFlags.None, Path_OnTarget);
|
||||
e.Mobile.SendMessage("Target a location and a path will be drawn there.");
|
||||
}
|
||||
var alg = OverrideAlgorithm ?? BitmapAStarAlgorithm.Instance;
|
||||
|
||||
private static void Path(Mobile from, IPoint3D p, PathAlgorithm alg, string name, int zOffset)
|
||||
{
|
||||
OverrideAlgorithm = alg;
|
||||
|
||||
var watch = new Stopwatch();
|
||||
watch.Start();
|
||||
var path = new MovementPath(from, new Point3D(p));
|
||||
watch.Stop();
|
||||
|
||||
if (!path.Success)
|
||||
if (alg?.CheckCondition(m, map, start, goal) == true)
|
||||
{
|
||||
from.SendMessage($"{name} path failed: {watch.ElapsedMilliseconds}ms");
|
||||
}
|
||||
else
|
||||
{
|
||||
from.SendMessage($"{name} path success: {watch.ElapsedMilliseconds}ms");
|
||||
|
||||
var x = from.X;
|
||||
var y = from.Y;
|
||||
var z = from.Z;
|
||||
|
||||
WayPoint waypoint = null;
|
||||
|
||||
for (var i = 0; i < path.Directions.Length; ++i)
|
||||
{
|
||||
Movement.Movement.Offset(path.Directions[i], ref x, ref y);
|
||||
|
||||
waypoint = new WayPoint(waypoint);
|
||||
waypoint.MoveToWorld(new Point3D(x, y, z + zOffset), from.Map);
|
||||
}
|
||||
Directions = alg.Find(m, map, start, goal);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Path_OnTarget(Mobile from, object targeted)
|
||||
catch (Exception e)
|
||||
{
|
||||
if (targeted is not IPoint3D p)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SpellHelper.GetSurfaceTop(ref p);
|
||||
|
||||
Path(from, p, BitmapAStarAlgorithm.Instance, "Bitmap", 0);
|
||||
OverrideAlgorithm = null;
|
||||
Console.WriteLine("Warning: {0}: Pathing error from {1} to {2}", e.GetType().Name, start, goal);
|
||||
}
|
||||
}
|
||||
|
||||
public Map Map { get; }
|
||||
|
||||
public Point3D Start { get; }
|
||||
|
||||
public Point3D Goal { get; }
|
||||
|
||||
public Direction[] Directions { get; }
|
||||
|
||||
public bool Success => Directions?.Length > 0;
|
||||
|
||||
public static PathAlgorithm OverrideAlgorithm { get; set; }
|
||||
|
||||
public static void Configure()
|
||||
{
|
||||
CommandSystem.Register("Path", AccessLevel.GameMaster, Path_OnCommand);
|
||||
CacheEvictionTimer.Configure();
|
||||
PathCacheCommands.Configure();
|
||||
}
|
||||
|
||||
[Usage("Path")]
|
||||
[Description("Draws a path from your current location to a targeted location.")]
|
||||
public static void Path_OnCommand(CommandEventArgs e)
|
||||
{
|
||||
e.Mobile.BeginTarget(-1, true, TargetFlags.None, Path_OnTarget);
|
||||
e.Mobile.SendMessage("Target a location and a path will be drawn there.");
|
||||
}
|
||||
|
||||
private static void Path(Mobile from, IPoint3D p, PathAlgorithm alg, string name, int zOffset)
|
||||
{
|
||||
OverrideAlgorithm = alg;
|
||||
|
||||
var watch = new Stopwatch();
|
||||
watch.Start();
|
||||
var path = new MovementPath(from, new Point3D(p));
|
||||
watch.Stop();
|
||||
|
||||
if (!path.Success)
|
||||
{
|
||||
from.SendMessage($"{name} path failed: {watch.ElapsedMilliseconds}ms");
|
||||
}
|
||||
else
|
||||
{
|
||||
from.SendMessage($"{name} path success: {watch.ElapsedMilliseconds}ms");
|
||||
|
||||
var x = from.X;
|
||||
var y = from.Y;
|
||||
var z = from.Z;
|
||||
|
||||
WayPoint waypoint = null;
|
||||
|
||||
for (var i = 0; i < path.Directions.Length; ++i)
|
||||
{
|
||||
Movement.Movement.Offset(path.Directions[i], ref x, ref y);
|
||||
|
||||
waypoint = new WayPoint(waypoint);
|
||||
waypoint.MoveToWorld(new Point3D(x, y, z + zOffset), from.Map);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void Path_OnTarget(Mobile from, object targeted)
|
||||
{
|
||||
if (targeted is not IPoint3D p)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SpellHelper.GetSurfaceTop(ref p);
|
||||
|
||||
Path(from, p, BitmapAStarAlgorithm.Instance, "Bitmap", 0);
|
||||
OverrideAlgorithm = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +1,29 @@
|
|||
namespace Server.PathAlgorithms
|
||||
namespace Server.PathAlgorithms;
|
||||
|
||||
public abstract class PathAlgorithm
|
||||
{
|
||||
public abstract class PathAlgorithm
|
||||
private static readonly Direction[] _calcDirections =
|
||||
{
|
||||
private static readonly Direction[] _calcDirections =
|
||||
{
|
||||
Direction.Up,
|
||||
Direction.North,
|
||||
Direction.Right,
|
||||
Direction.West,
|
||||
Direction.North,
|
||||
Direction.East,
|
||||
Direction.Left,
|
||||
Direction.South,
|
||||
Direction.Down
|
||||
};
|
||||
Direction.Up,
|
||||
Direction.North,
|
||||
Direction.Right,
|
||||
Direction.West,
|
||||
Direction.North,
|
||||
Direction.East,
|
||||
Direction.Left,
|
||||
Direction.South,
|
||||
Direction.Down
|
||||
};
|
||||
|
||||
public abstract bool CheckCondition(Mobile m, Map map, Point3D start, Point3D goal);
|
||||
public abstract Direction[] Find(Mobile m, Map map, Point3D start, Point3D goal);
|
||||
public abstract bool CheckCondition(Mobile m, Map map, Point3D start, Point3D goal);
|
||||
public abstract Direction[] Find(Mobile m, Map map, Point3D start, Point3D goal);
|
||||
|
||||
public static Direction GetDirection(int xSource, int ySource, int xDest, int yDest)
|
||||
{
|
||||
var x = xDest + 1 - xSource;
|
||||
var y = yDest + 1 - ySource;
|
||||
var v = y * 3 + x;
|
||||
public static Direction GetDirection(int xSource, int ySource, int xDest, int yDest)
|
||||
{
|
||||
var x = xDest + 1 - xSource;
|
||||
var y = yDest + 1 - ySource;
|
||||
var v = y * 3 + x;
|
||||
|
||||
if (v is < 0 or >= 9)
|
||||
{
|
||||
return Direction.North;
|
||||
}
|
||||
|
||||
return _calcDirections[v];
|
||||
}
|
||||
return v is < 0 or >= 9 ? Direction.North : _calcDirections[v];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using Server.Engines.Pathing.Cache;
|
||||
|
||||
|
|
@ -95,12 +96,12 @@ public static class PathCacheCommands
|
|||
|
||||
var totalChunks = 0;
|
||||
var totalMaps = 0;
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
for (var i = 0; i < Map.Maps.Length; i++)
|
||||
{
|
||||
var map = Map.Maps[i];
|
||||
if (map == null || map == Map.Internal || (only.HasValue && map.MapID != only.Value))
|
||||
if (map == null || map == Map.Internal || only.HasValue && map.MapID != only.Value)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,155 +1,152 @@
|
|||
using System;
|
||||
using CalcMoves = Server.Movement.Movement;
|
||||
|
||||
namespace Server
|
||||
namespace Server;
|
||||
|
||||
public class PathFollower
|
||||
{
|
||||
public class PathFollower
|
||||
private static bool Enabled;
|
||||
private static readonly TimeSpan RepathDelay = TimeSpan.FromSeconds(2.0);
|
||||
|
||||
private readonly Mobile m_From;
|
||||
private int m_Index;
|
||||
private DateTime m_LastPathTime;
|
||||
private Point3D m_Next, m_LastGoalLoc;
|
||||
private MovementPath m_Path;
|
||||
|
||||
public PathFollower(Mobile from, IPoint3D goal)
|
||||
{
|
||||
private static bool Enabled;
|
||||
private static readonly TimeSpan RepathDelay = TimeSpan.FromSeconds(2.0);
|
||||
m_From = from;
|
||||
Goal = goal;
|
||||
}
|
||||
|
||||
private readonly Mobile m_From;
|
||||
private int m_Index;
|
||||
private DateTime m_LastPathTime;
|
||||
private Point3D m_Next, m_LastGoalLoc;
|
||||
private MovementPath m_Path;
|
||||
public MoveMethod Mover { get; set; }
|
||||
|
||||
public PathFollower(Mobile from, IPoint3D goal)
|
||||
public IPoint3D Goal { get; }
|
||||
|
||||
public static void Configure()
|
||||
{
|
||||
Enabled = ServerConfiguration.GetOrUpdateSetting("pathfinding.enable", true);
|
||||
}
|
||||
|
||||
public MoveResult Move(Direction d) =>
|
||||
Mover?.Invoke(d, true) ?? (m_From.Move(d) ? MoveResult.Success : MoveResult.Blocked);
|
||||
|
||||
public Point3D GetGoalLocation() => (Goal as Item)?.GetWorldLocation() ?? new Point3D(Goal);
|
||||
|
||||
public void Advance(ref Point3D p, int index)
|
||||
{
|
||||
if (m_Path?.Success == true)
|
||||
{
|
||||
m_From = from;
|
||||
Goal = goal;
|
||||
}
|
||||
var dirs = m_Path.Directions;
|
||||
|
||||
public MoveMethod Mover { get; set; }
|
||||
|
||||
public IPoint3D Goal { get; }
|
||||
|
||||
public static void Configure()
|
||||
{
|
||||
Enabled = ServerConfiguration.GetOrUpdateSetting("pathfinding.enable", true);
|
||||
}
|
||||
|
||||
public MoveResult Move(Direction d) =>
|
||||
Mover?.Invoke(d, true) ?? (m_From.Move(d) ? MoveResult.Success : MoveResult.Blocked);
|
||||
|
||||
public Point3D GetGoalLocation() => (Goal as Item)?.GetWorldLocation() ?? new Point3D(Goal);
|
||||
|
||||
public void Advance(ref Point3D p, int index)
|
||||
{
|
||||
if (m_Path?.Success == true)
|
||||
if (index >= 0 && index < dirs.Length)
|
||||
{
|
||||
var dirs = m_Path.Directions;
|
||||
|
||||
if (index >= 0 && index < dirs.Length)
|
||||
{
|
||||
CalcMoves.Offset(dirs[index], ref p);
|
||||
}
|
||||
CalcMoves.Offset(dirs[index], ref p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ForceRepath()
|
||||
public void ForceRepath()
|
||||
{
|
||||
m_Path = null;
|
||||
}
|
||||
|
||||
public bool CheckPath()
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
m_Path = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool CheckPath()
|
||||
var goal = GetGoalLocation();
|
||||
|
||||
if (m_Path != null && (m_Path.Success && goal == m_LastGoalLoc || m_LastPathTime + RepathDelay > Core.Now) &&
|
||||
!(m_Path.Success && Check(m_From.Location, m_LastGoalLoc, 0)))
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
var goal = GetGoalLocation();
|
||||
m_LastPathTime = Core.Now;
|
||||
m_LastGoalLoc = goal;
|
||||
|
||||
if (m_Path != null && (m_Path.Success && goal == m_LastGoalLoc || m_LastPathTime + RepathDelay > Core.Now) &&
|
||||
!(m_Path.Success && Check(m_From.Location, m_LastGoalLoc, 0)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_Path = new MovementPath(m_From, goal);
|
||||
|
||||
m_LastPathTime = Core.Now;
|
||||
m_LastGoalLoc = goal;
|
||||
m_Index = 0;
|
||||
m_Next = m_From.Location;
|
||||
|
||||
m_Path = new MovementPath(m_From, goal);
|
||||
Advance(ref m_Next, m_Index);
|
||||
|
||||
m_Index = 0;
|
||||
m_Next = m_From.Location;
|
||||
return true;
|
||||
}
|
||||
|
||||
Advance(ref m_Next, m_Index);
|
||||
public static bool Check(Point3D loc, Point3D goal, int range) =>
|
||||
Utility.InRange(loc, goal, range) && (range > 1 || (loc.Z - goal.Z).Abs() < 16);
|
||||
|
||||
public bool Follow(bool run, int range)
|
||||
{
|
||||
var goal = GetGoalLocation();
|
||||
Direction d;
|
||||
|
||||
if (Check(m_From.Location, goal, range))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool Check(Point3D loc, Point3D goal, int range) =>
|
||||
Utility.InRange(loc, goal, range) && (range > 1 || (loc.Z - goal.Z).Abs() < 16);
|
||||
var repathed = CheckPath();
|
||||
|
||||
public bool Follow(bool run, int range)
|
||||
if (!(Enabled && m_Path.Success))
|
||||
{
|
||||
var goal = GetGoalLocation();
|
||||
Direction d;
|
||||
|
||||
if (Check(m_From.Location, goal, range))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var repathed = CheckPath();
|
||||
|
||||
if (!(Enabled && m_Path.Success))
|
||||
{
|
||||
d = m_From.GetDirectionTo(goal, run);
|
||||
m_From.SetDirection(d);
|
||||
|
||||
return Move(d) is MoveResult.Success or MoveResult.SuccessAutoTurn
|
||||
&& Check(m_From.Location, goal, range);
|
||||
}
|
||||
|
||||
d = m_From.GetDirectionTo(m_Next, run);
|
||||
d = m_From.GetDirectionTo(goal, run);
|
||||
m_From.SetDirection(d);
|
||||
var res = Move(d);
|
||||
|
||||
if (res == MoveResult.Blocked)
|
||||
return Move(d) is MoveResult.Success or MoveResult.SuccessAutoTurn && Check(m_From.Location, goal, range);
|
||||
}
|
||||
|
||||
d = m_From.GetDirectionTo(m_Next, run);
|
||||
m_From.SetDirection(d);
|
||||
var res = Move(d);
|
||||
|
||||
if (res == MoveResult.Blocked)
|
||||
{
|
||||
if (repathed)
|
||||
{
|
||||
if (repathed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
m_Path = null;
|
||||
CheckPath();
|
||||
m_Path = null;
|
||||
CheckPath();
|
||||
|
||||
if (!m_Path!.Success)
|
||||
{
|
||||
d = m_From.GetDirectionTo(goal);
|
||||
m_From.SetDirection(d);
|
||||
|
||||
return Move(d) is MoveResult.Success or MoveResult.SuccessAutoTurn
|
||||
&& Check(m_From.Location, goal, range);
|
||||
}
|
||||
|
||||
d = m_From.GetDirectionTo(m_Next);
|
||||
if (!m_Path!.Success)
|
||||
{
|
||||
d = m_From.GetDirectionTo(goal);
|
||||
m_From.SetDirection(d);
|
||||
|
||||
if (Move(d) == MoveResult.Blocked)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return Move(d) is MoveResult.Success or MoveResult.SuccessAutoTurn && Check(m_From.Location, goal, range);
|
||||
}
|
||||
|
||||
if (m_From.X == m_Next.X && m_From.Y == m_Next.Y)
|
||||
d = m_From.GetDirectionTo(m_Next);
|
||||
m_From.SetDirection(d);
|
||||
|
||||
if (Move(d) == MoveResult.Blocked)
|
||||
{
|
||||
if (m_From.Z == m_Next.Z)
|
||||
{
|
||||
++m_Index;
|
||||
Advance(ref m_Next, m_Index);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Path = null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return Check(m_From.Location, goal, range);
|
||||
}
|
||||
|
||||
if (m_From.X == m_Next.X && m_From.Y == m_Next.Y)
|
||||
{
|
||||
if (m_From.Z == m_Next.Z)
|
||||
{
|
||||
++m_Index;
|
||||
Advance(ref m_Next, m_Index);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Path = null;
|
||||
}
|
||||
}
|
||||
|
||||
return Check(m_From.Location, goal, range);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue