feat(pathfinding): Tier 4 multi-Z strata (file format v2) (#2450)
## Summary Multi-Z cells (bridges, stairs, paver-over-ground, multi-floor structures) now carry **per-stratum walkability data** in the cache instead of falling through to the slow path. The data is computed at chunk-build time, persisted in the `.swb` file, and selected at query time by matching the request's `sourceZ` against each stratum's `zCenter` (within `StepHeight` tolerance). This is the Tier 4 strata feature, deferred from PR #2447 / PR #2448 / PR #2449. Builds on PR #2449's lazy backing store and public bake helpers. ## Wire format change (v1 → v2) `StepCacheFile.FormatVersion = 2`. `MinSupportedVersion = 2`. v1 `.swb` files are silently rejected at open time (treated as missing) and overwritten on the next `SaveToFile` / `BakeMap`. **No migration** — older files just get re-baked. The `MinSupportedVersion` sentinel is the model going forward: bump the constant when an incompatible change lands; admins re-bake on the next deploy. No matrix of v1↔v2↔v3 migration logic to maintain. ## What changed - **`StepProbe.ComputeStrataAt(map, x, y)`** — enumerates walkable standing-Zs at the cell (one per land surface plus one per walkable static), collapses Zs within `2*StepHeight`, runs `ComputeMaskAt` at each surviving Z. Returns `null` for single-Z cells (caller uses the chunk's main mask). - **`StepChunk`** — replaces the old `MultiZCells` bitmap with a **strata storage pair**: - `ushort[256] StrataOffsetByCell` (sentinel `NoStrata = 0xFFFF` = "no strata for that cell") - `byte[] StrataData` packed: `u8 stratumCount`, then `count × 19-byte stratum` - `sbyte zCenter, byte walkMask, byte wetMask, sbyte walkZ_N..NW (8), sbyte swimZ_N..NW (8)` - `IsCellMultiZ` derives from `StrataOffsetByCell[cell] != NoStrata` — same semantics, single source of truth. - **`StepCache.BuildChunk`** — populates strata for cells flagged multi-Z via `SetStrata`. Chunks with zero multi-Z cells pay zero strata overhead (offset array + data array stay null). - **`StepCache.TryGetMask`** — for multi-Z cells, scans strata with `TryStratumHit`; returns the matching one with `HitKind=Hit`. Falls through to slow path only when no stratum matches the query `sourceZ`. - **`StepCacheFile`** — v2 serialization with strata trailer per chunk + `recordLength` in index entry. Lazy reader sizes scratch per-chunk-record using the recorded length, growing on demand for multi-Z-heavy chunks. Patches `IndexOffset` on `w.Buffer` (BufferWriter's current backing array) since it grows during variable-size chunk writes. ## File layout v2 ``` Header (48 bytes): u32 Magic = 0x42575300 ('SWB\0') u32 Version = 2 u32 MapId u64 Fingerprint XxHash3 over LandTable + ItemTable flags + map files (mapX.mul/.uop, staidxX.mul, staticsX.mul) u64 BakeTimestamp informational u32 ChunkCount u64 IndexOffset position where chunk index begins Per chunk (variable size): u16 ChunkX, ChunkY u32 BuiltMultisVersion u8 HasStrata 0 = no strata trailer; 1 = strata trailer follows byte WalkMask[256], WetMask[256] sbyte SourceZ[256], WalkZN..WalkZNW[256], SwimZN..SwimZNW[256] // Strata trailer (only when HasStrata == 1): u16 StrataOffsetByCell[256] // NoStrata sentinel = 0xFFFF u32 StrataDataLength byte StrataData[StrataDataLength] Per multi-Z cell: u8 count, then count × Stratum (19 bytes) Index trailer (20 × ChunkCount bytes): per chunk: { u64 chunkKey, u64 fileOffset, u32 recordLength } ```
This commit is contained in:
parent
a8ca82738d
commit
7bd2cb6a2a
5 changed files with 471 additions and 73 deletions
|
|
@ -1,9 +1,11 @@
|
|||
using System;
|
||||
|
||||
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 bitmap) and LRU bookkeeping.
|
||||
/// version, multi-Z strata) and LRU bookkeeping.
|
||||
/// </summary>
|
||||
internal sealed class StepChunk
|
||||
{
|
||||
|
|
@ -35,11 +37,23 @@ internal sealed class StepChunk
|
|||
public readonly sbyte[] SwimZW = new sbyte[CellsPerChunk];
|
||||
public readonly sbyte[] SwimZNW = new sbyte[CellsPerChunk];
|
||||
|
||||
/// <summary>Sentinel: cell has no strata — single-Z, use the main Walk/Wet arrays.</summary>
|
||||
public const ushort NoStrata = ushort.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// 32 bytes = 256 bits when allocated. Lazy: most chunks are entirely single-Z,
|
||||
/// so we only pay the 32 bytes on chunks that actually need it.
|
||||
/// Length-256 offset table: <c>StrataOffsetByCell[cell] = byte offset</c> into
|
||||
/// <see cref="StrataData"/> where this cell's strata begin, or <see cref="NoStrata"/>
|
||||
/// for cells without multi-Z. Null when the chunk has zero multi-Z cells.
|
||||
/// </summary>
|
||||
private byte[] _multiZCells;
|
||||
private ushort[] _strataOffsetByCell;
|
||||
|
||||
/// <summary>
|
||||
/// Packed per-cell strata. For each cell with strata:
|
||||
/// u8 stratumCount, then stratumCount × Stratum (19 bytes each):
|
||||
/// sbyte zCenter, byte walkMask, byte wetMask,
|
||||
/// sbyte walkZ_N..NW (8), sbyte swimZ_N..NW (8)
|
||||
/// </summary>
|
||||
private byte[] _strataData;
|
||||
|
||||
/// <summary>Snapshot of Sector.MultisVersion at the time this chunk was built.</summary>
|
||||
public int BuiltMultisVersion;
|
||||
|
|
@ -47,24 +61,32 @@ internal sealed class StepChunk
|
|||
/// <summary>Updated on every cache hit/miss. Used by LRU fallback eviction.</summary>
|
||||
public long LastTouchedTicks;
|
||||
|
||||
public bool IsCellMultiZ(int cellIndex) =>
|
||||
_multiZCells != null && (_multiZCells[cellIndex >> 3] & (1 << (cellIndex & 7))) != 0;
|
||||
/// <summary>Size in bytes of one Stratum record in StrataData.</summary>
|
||||
public const int StratumByteLength = 1 + 1 + 1 + 8 + 8;
|
||||
|
||||
public void MarkCellMultiZ(int cellIndex)
|
||||
public bool IsCellMultiZ(int cellIndex) => GetStrataOffset(cellIndex) != NoStrata;
|
||||
|
||||
public ushort GetStrataOffset(int cellIndex) =>
|
||||
_strataOffsetByCell == null ? NoStrata : _strataOffsetByCell[cellIndex];
|
||||
|
||||
public ReadOnlySpan<byte> StrataData =>
|
||||
_strataData == null ? ReadOnlySpan<byte>.Empty : _strataData.AsSpan();
|
||||
|
||||
/// <summary>
|
||||
/// Single-shot setter for the chunk's strata. Pass null/null to clear (chunk becomes
|
||||
/// "no multi-Z"). Otherwise <paramref name="offsetByCell"/> must be length 256 with
|
||||
/// <see cref="NoStrata"/> for cells without strata, and <paramref name="data"/> the
|
||||
/// packed strata records.
|
||||
/// </summary>
|
||||
internal void SetStrata(ushort[] offsetByCell, byte[] data)
|
||||
{
|
||||
_multiZCells ??= new byte[32];
|
||||
_multiZCells[cellIndex >> 3] |= (byte)(1 << (cellIndex & 7));
|
||||
_strataOffsetByCell = offsetByCell;
|
||||
_strataData = data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialization hook for <see cref="StepCacheFile"/>: returns the multi-Z bitmap,
|
||||
/// or null if no cells in this chunk are multi-Z. Read-only — callers must not mutate.
|
||||
/// </summary>
|
||||
internal byte[] GetMultiZCellsForSerialization() => _multiZCells;
|
||||
/// <summary>Serialization hook: returns the raw offset array (or null if no strata).</summary>
|
||||
internal ushort[] GetStrataOffsetByCellForSerialization() => _strataOffsetByCell;
|
||||
|
||||
/// <summary>
|
||||
/// Deserialization hook: assigns the multi-Z bitmap from a file load. Caller is
|
||||
/// responsible for passing a 32-byte array (or null for "no cells multi-Z").
|
||||
/// </summary>
|
||||
internal void RestoreMultiZCellsFromSerialization(byte[] multiZ) => _multiZCells = multiZ;
|
||||
/// <summary>Serialization hook: returns the raw data array (or null if no strata).</summary>
|
||||
internal byte[] GetStrataDataForSerialization() => _strataData;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue