ModernUO/Projects/UOContent/Engines/Pathing/Cache/StepChunk.cs
Kamron Batman 7bd2cb6a2a
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 }
```
2026-05-06 22:55:42 -07:00

92 lines
4.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 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>
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>
public readonly byte[] WetMask = new byte[CellsPerChunk];
public readonly sbyte[] SourceZ = new sbyte[CellsPerChunk];
public readonly sbyte[] WalkZN = new sbyte[CellsPerChunk];
public readonly sbyte[] WalkZNE = new sbyte[CellsPerChunk];
public readonly sbyte[] WalkZE = new sbyte[CellsPerChunk];
public readonly sbyte[] WalkZSE = new sbyte[CellsPerChunk];
public readonly sbyte[] WalkZS = new sbyte[CellsPerChunk];
public readonly sbyte[] WalkZSW = new sbyte[CellsPerChunk];
public readonly sbyte[] WalkZW = new sbyte[CellsPerChunk];
public readonly sbyte[] WalkZNW = new sbyte[CellsPerChunk];
public readonly sbyte[] SwimZN = new sbyte[CellsPerChunk];
public readonly sbyte[] SwimZNE = new sbyte[CellsPerChunk];
public readonly sbyte[] SwimZE = new sbyte[CellsPerChunk];
public readonly sbyte[] SwimZSE = new sbyte[CellsPerChunk];
public readonly sbyte[] SwimZS = new sbyte[CellsPerChunk];
public readonly sbyte[] SwimZSW = new sbyte[CellsPerChunk];
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>
/// 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 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;
/// <summary>Updated on every cache hit/miss. Used by LRU fallback eviction.</summary>
public long LastTouchedTicks;
/// <summary>Size in bytes of one Stratum record in StrataData.</summary>
public const int StratumByteLength = 1 + 1 + 1 + 8 + 8;
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)
{
_strataOffsetByCell = offsetByCell;
_strataData = data;
}
/// <summary>Serialization hook: returns the raw offset array (or null if no strata).</summary>
internal ushort[] GetStrataOffsetByCellForSerialization() => _strataOffsetByCell;
/// <summary>Serialization hook: returns the raw data array (or null if no strata).</summary>
internal byte[] GetStrataDataForSerialization() => _strataData;
}