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
|
|
@ -139,9 +139,17 @@ public class StepCacheLifecycleTests
|
|||
Assert.True(chunks.ContainsKey(key));
|
||||
var chunk = chunks[key];
|
||||
|
||||
// Mark cell at (1500, 1600) within the chunk as multi-Z.
|
||||
// Inject "this cell has strata but none match the query Z" — proves the cache
|
||||
// still falls through to slow path when no stratum can answer.
|
||||
var cellIndex = ((1600 - ((1600 >> 4) << 4)) << 4) | (1500 - ((1500 >> 4) << 4));
|
||||
chunk.MarkCellMultiZ(cellIndex);
|
||||
var offsets = new ushort[StepChunk.CellsPerChunk];
|
||||
for (var i = 0; i < offsets.Length; i++)
|
||||
{
|
||||
offsets[i] = StepChunk.NoStrata;
|
||||
}
|
||||
offsets[cellIndex] = 0; // points to a 0-stratum-count entry → no match
|
||||
var data = new byte[] { 0 };
|
||||
chunk.SetStrata(offsets, data);
|
||||
|
||||
var lookup = cache.TryGetMask(map, 1500, 1600, 10);
|
||||
|
||||
|
|
@ -152,6 +160,86 @@ public class StepCacheLifecycleTests
|
|||
Assert.Equal(preInjectionFallthroughMultiZ + 1L, stats.FallthroughMultiZ);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tier4Strata_MatchingZ_ReturnsHitFromStratum()
|
||||
{
|
||||
var cache = StepCache.Instance;
|
||||
cache.Clear();
|
||||
|
||||
var map = Map.Maps[1];
|
||||
cache.TryGetMask(map, 1500, 1600, 10);
|
||||
|
||||
var chunksField = typeof(StepCache).GetField(
|
||||
"_chunks",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance
|
||||
);
|
||||
var chunks = (System.Collections.Generic.Dictionary<long, StepChunk>)chunksField.GetValue(cache);
|
||||
var key = StepCache.EncodeKey(map.MapID, 1500 >> 4, 1600 >> 4);
|
||||
var chunk = chunks[key];
|
||||
|
||||
// Inject one stratum at zCenter=42, walkMask=0b00000011 (N + NE).
|
||||
// Query at sourceZ=42 must hit and return that stratum's data.
|
||||
var cellIndex = ((1600 - ((1600 >> 4) << 4)) << 4) | (1500 - ((1500 >> 4) << 4));
|
||||
var offsets = new ushort[StepChunk.CellsPerChunk];
|
||||
for (var i = 0; i < offsets.Length; i++)
|
||||
{
|
||||
offsets[i] = StepChunk.NoStrata;
|
||||
}
|
||||
offsets[cellIndex] = 0;
|
||||
|
||||
var data = new byte[1 + StepChunk.StratumByteLength];
|
||||
data[0] = 1; // count
|
||||
data[1] = 42; // zCenter
|
||||
data[2] = 0b0000_0011; // walkMask (N | NE)
|
||||
data[3] = 0; // wetMask
|
||||
data[4] = 42; data[5] = 42; data[6] = 0; data[7] = 0;
|
||||
data[8] = 0; data[9] = 0; data[10] = 0; data[11] = 0;
|
||||
data[12] = 0; data[13] = 0; data[14] = 0; data[15] = 0;
|
||||
data[16] = 0; data[17] = 0; data[18] = 0; data[19] = 0;
|
||||
chunk.SetStrata(offsets, data);
|
||||
|
||||
var lookup = cache.TryGetMask(map, 1500, 1600, 42);
|
||||
Assert.True(lookup.IsHit);
|
||||
Assert.Equal((byte)0b0000_0011, lookup.WalkMask);
|
||||
Assert.Equal((sbyte)42, lookup.WalkZ_N);
|
||||
Assert.Equal((sbyte)42, lookup.WalkZ_NE);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tier4Strata_NonMatchingZ_FallsThrough()
|
||||
{
|
||||
var cache = StepCache.Instance;
|
||||
cache.Clear();
|
||||
|
||||
var map = Map.Maps[1];
|
||||
cache.TryGetMask(map, 1500, 1600, 10);
|
||||
|
||||
var chunksField = typeof(StepCache).GetField(
|
||||
"_chunks",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance
|
||||
);
|
||||
var chunks = (System.Collections.Generic.Dictionary<long, StepChunk>)chunksField.GetValue(cache);
|
||||
var key = StepCache.EncodeKey(map.MapID, 1500 >> 4, 1600 >> 4);
|
||||
var chunk = chunks[key];
|
||||
|
||||
// Stratum at zCenter=42; query at sourceZ=10 (delta > StepHeight=2). Must fallthrough.
|
||||
var cellIndex = ((1600 - ((1600 >> 4) << 4)) << 4) | (1500 - ((1500 >> 4) << 4));
|
||||
var offsets = new ushort[StepChunk.CellsPerChunk];
|
||||
for (var i = 0; i < offsets.Length; i++)
|
||||
{
|
||||
offsets[i] = StepChunk.NoStrata;
|
||||
}
|
||||
offsets[cellIndex] = 0;
|
||||
|
||||
var data = new byte[1 + StepChunk.StratumByteLength];
|
||||
data[0] = 1; data[1] = 42; // zCenter=42, all other bytes 0
|
||||
chunk.SetStrata(offsets, data);
|
||||
|
||||
var lookup = cache.TryGetMask(map, 1500, 1600, 10);
|
||||
Assert.False(lookup.IsHit);
|
||||
Assert.Equal(CacheHitKind.Fallthrough_MultiZ, lookup.HitKind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LruCap_OverflowEvictsToCap()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue