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
|
|
@ -348,6 +348,18 @@ public sealed class StepCache
|
|||
|
||||
if (chunk.IsCellMultiZ(cellIndex))
|
||||
{
|
||||
// Tier 4: try the per-cell strata. Each stratum is keyed by its bake-time
|
||||
// standing-Z; a query matches when |sourceZ - stratum.zCenter| <= StepHeight.
|
||||
if (TryStratumHit(chunk, cellIndex, sourceZ, hitKindResult, out var stratumResult))
|
||||
{
|
||||
switch (hitKindResult)
|
||||
{
|
||||
case CacheHitKind.Miss_NotBuilt: { _missesNotBuilt++; break; }
|
||||
case CacheHitKind.Miss_DirtyRebuild: { _missesDirtyRebuild++; break; }
|
||||
case CacheHitKind.Hit: { _hits++; break; }
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
|
@ -425,6 +437,11 @@ public sealed class StepCache
|
|||
var baseX = chunkX << 4;
|
||||
var baseY = chunkY << 4;
|
||||
|
||||
// Tier 4 strata accumulator. Lazily allocated when the first multi-Z cell
|
||||
// appears; otherwise the chunk has zero strata overhead.
|
||||
ushort[] strataOffsetByCell = null;
|
||||
System.Collections.Generic.List<byte> strataData = null;
|
||||
|
||||
for (var dy = 0; dy < ChunkSize; dy++)
|
||||
{
|
||||
for (var dx = 0; dx < ChunkSize; dx++)
|
||||
|
|
@ -462,19 +479,145 @@ public sealed class StepCache
|
|||
chunk.SwimZW[cell] = result.SwimZ_W;
|
||||
chunk.SwimZNW[cell] = result.SwimZ_NW;
|
||||
|
||||
// Multi-Z = ≥2 surfaces reachable from standingZ. Mirrors the baker's
|
||||
// CheckStaticStep filter so we don't over-mark.
|
||||
// Multi-Z handling: if the cell has 2+ reachable surfaces, compute its
|
||||
// Tier 4 strata so future queries can be answered without falling through
|
||||
// to the slow path. ComputeStrataAt returns null for single-Z cells.
|
||||
if (CountReachableSurfaces(map, x, y, standingZ) > 1)
|
||||
{
|
||||
chunk.MarkCellMultiZ(cell);
|
||||
var strata = StepProbe.ComputeStrataAt(map, x, y);
|
||||
if (strata != null)
|
||||
{
|
||||
if (strataOffsetByCell == null)
|
||||
{
|
||||
strataOffsetByCell = new ushort[StepChunk.CellsPerChunk];
|
||||
for (var i = 0; i < strataOffsetByCell.Length; i++)
|
||||
{
|
||||
strataOffsetByCell[i] = StepChunk.NoStrata;
|
||||
}
|
||||
strataData = new System.Collections.Generic.List<byte>(256);
|
||||
}
|
||||
|
||||
// Cap at 65,535 byte offsets — well above realistic per-chunk
|
||||
// strata volume. If we ever blow past this we'd silently truncate;
|
||||
// assert as a defensive guard.
|
||||
if (strataData.Count > ushort.MaxValue - StepChunk.StratumByteLength * 8)
|
||||
{
|
||||
// Should never happen for sane tile data; bail to fallthrough.
|
||||
}
|
||||
else
|
||||
{
|
||||
strataOffsetByCell[cell] = (ushort)strataData.Count;
|
||||
strataData.Add((byte)strata.Length);
|
||||
for (var s = 0; s < strata.Length; s++)
|
||||
{
|
||||
AppendStratumBytes(strataData, strata[s]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (strataOffsetByCell != null)
|
||||
{
|
||||
chunk.SetStrata(strataOffsetByCell, strataData.ToArray());
|
||||
}
|
||||
|
||||
_buildsTotal++;
|
||||
return chunk;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tier 4 strata lookup. Walks the cell's stratum list, returns true with the first
|
||||
/// stratum whose <c>zCenter</c> is within StepHeight of <paramref name="sourceZ"/>.
|
||||
/// Layout matches <see cref="StepChunk.StrataData"/>: u8 count, then count × 19-byte
|
||||
/// stratum (sbyte zCenter, byte walkMask, byte wetMask, 8 sbyte walkZ, 8 sbyte swimZ).
|
||||
/// </summary>
|
||||
private static bool TryStratumHit(
|
||||
StepChunk chunk, int cellIndex, sbyte sourceZ, CacheHitKind hitKind, out StepMask result
|
||||
)
|
||||
{
|
||||
var off = chunk.GetStrataOffset(cellIndex);
|
||||
if (off == StepChunk.NoStrata)
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var data = chunk.StrataData;
|
||||
if (off >= data.Length)
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var count = data[off];
|
||||
var entryStart = off + 1;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var entryOff = entryStart + i * StepChunk.StratumByteLength;
|
||||
if (entryOff + StepChunk.StratumByteLength > data.Length)
|
||||
{
|
||||
break;
|
||||
}
|
||||
var zCenter = (sbyte)data[entryOff];
|
||||
if (Math.Abs(sourceZ - zCenter) > StepHeight)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
result = new StepMask(
|
||||
/* walkMask */ data[entryOff + 1],
|
||||
/* wetMask */ data[entryOff + 2],
|
||||
(sbyte)data[entryOff + 3],
|
||||
(sbyte)data[entryOff + 4],
|
||||
(sbyte)data[entryOff + 5],
|
||||
(sbyte)data[entryOff + 6],
|
||||
(sbyte)data[entryOff + 7],
|
||||
(sbyte)data[entryOff + 8],
|
||||
(sbyte)data[entryOff + 9],
|
||||
(sbyte)data[entryOff + 10],
|
||||
(sbyte)data[entryOff + 11],
|
||||
(sbyte)data[entryOff + 12],
|
||||
(sbyte)data[entryOff + 13],
|
||||
(sbyte)data[entryOff + 14],
|
||||
(sbyte)data[entryOff + 15],
|
||||
(sbyte)data[entryOff + 16],
|
||||
(sbyte)data[entryOff + 17],
|
||||
(sbyte)data[entryOff + 18],
|
||||
hitKind
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void AppendStratumBytes(
|
||||
System.Collections.Generic.List<byte> dst, in StepProbe.ComputedStratum s
|
||||
)
|
||||
{
|
||||
dst.Add((byte)s.ZCenter);
|
||||
dst.Add(s.Mask.WalkMask);
|
||||
dst.Add(s.Mask.WetMask);
|
||||
dst.Add((byte)s.Mask.WalkZ_N);
|
||||
dst.Add((byte)s.Mask.WalkZ_NE);
|
||||
dst.Add((byte)s.Mask.WalkZ_E);
|
||||
dst.Add((byte)s.Mask.WalkZ_SE);
|
||||
dst.Add((byte)s.Mask.WalkZ_S);
|
||||
dst.Add((byte)s.Mask.WalkZ_SW);
|
||||
dst.Add((byte)s.Mask.WalkZ_W);
|
||||
dst.Add((byte)s.Mask.WalkZ_NW);
|
||||
dst.Add((byte)s.Mask.SwimZ_N);
|
||||
dst.Add((byte)s.Mask.SwimZ_NE);
|
||||
dst.Add((byte)s.Mask.SwimZ_E);
|
||||
dst.Add((byte)s.Mask.SwimZ_SE);
|
||||
dst.Add((byte)s.Mask.SwimZ_S);
|
||||
dst.Add((byte)s.Mask.SwimZ_SW);
|
||||
dst.Add((byte)s.Mask.SwimZ_W);
|
||||
dst.Add((byte)s.Mask.SwimZ_NW);
|
||||
}
|
||||
|
||||
private const int PersonHeight = 16;
|
||||
private const int StepHeight = 2;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue