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
|
|
@ -19,6 +19,89 @@ public static class StepProbe
|
|||
private const int PersonHeight = 16;
|
||||
private const int StepHeight = 2;
|
||||
|
||||
public readonly struct ComputedStratum(sbyte zCenter, StepMask mask)
|
||||
{
|
||||
public readonly sbyte ZCenter = zCenter;
|
||||
public readonly StepMask Mask = mask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tier 4 strata builder: enumerates the distinct walkable standing-Zs at (x, y)
|
||||
/// — one per land surface plus one per walkable static — and runs
|
||||
/// <see cref="ComputeMaskAt"/> at each, producing a per-stratum walkability snapshot.
|
||||
/// Returns null when the cell has 0 or 1 strata (single-Z; the caller should use
|
||||
/// the chunk's main mask).
|
||||
/// </summary>
|
||||
public static ComputedStratum[] ComputeStrataAt(Map map, int x, int y)
|
||||
{
|
||||
if (map == null || map == Map.Internal)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (x < 0 || y < 0 || x >= map.Width || y >= map.Height)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Collect candidate Zs. 16 slots is generous — multi-Z cells in practice rarely
|
||||
// exceed 3-4 surfaces (bridge over land, paver-over-ground, multi-floor stairs).
|
||||
Span<int> zs = stackalloc int[16];
|
||||
var count = 0;
|
||||
|
||||
var landTile = map.Tiles.GetLandTile(x, y);
|
||||
var landFlags = TileData.LandTable[landTile.ID & TileData.MaxLandValue].Flags;
|
||||
if (!landTile.Ignored && (landFlags & TileFlag.Impassable) == 0)
|
||||
{
|
||||
map.GetAverageZ(x, y, out _, out var landCenter, out _);
|
||||
zs[count++] = landCenter;
|
||||
}
|
||||
|
||||
foreach (var tile in map.Tiles.GetStaticAndMultiTiles(x, y))
|
||||
{
|
||||
if (count >= zs.Length)
|
||||
{
|
||||
break;
|
||||
}
|
||||
var data = TileData.ItemTable[tile.ID & TileData.MaxItemValue];
|
||||
if (!data.Surface || data.Impassable)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
zs[count++] = tile.Z + data.CalcHeight;
|
||||
}
|
||||
|
||||
if (count <= 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sort and merge near-equal Zs. Two Zs separated by less than 2*StepHeight collapse
|
||||
// into a single stratum — the slow path's tolerance treats them as the same surface.
|
||||
zs[..count].Sort();
|
||||
Span<int> distinct = stackalloc int[16];
|
||||
var distinctCount = 0;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
if (distinctCount == 0 || zs[i] - distinct[distinctCount - 1] > 2 * StepHeight)
|
||||
{
|
||||
distinct[distinctCount++] = zs[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (distinctCount <= 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var strata = new ComputedStratum[distinctCount];
|
||||
for (var i = 0; i < distinctCount; i++)
|
||||
{
|
||||
var z = (sbyte)Math.Clamp(distinct[i], sbyte.MinValue, sbyte.MaxValue);
|
||||
strata[i] = new ComputedStratum(z, ComputeMaskAt(map, x, y, z));
|
||||
}
|
||||
return strata;
|
||||
}
|
||||
|
||||
public static StepMask ComputeMaskAt(Map map, int x, int y, sbyte sourceZ)
|
||||
{
|
||||
if (map == null || map == Map.Internal)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue