using System;
using System.Collections.Generic;
using System.IO;
using Server.Engines.Pathing.Cache;
using Xunit;
namespace Server.Tests.Pathfinding;
///
/// The .swb encoding, exercised through Write → OpenForLazy → TryReadChunk. Three transforms stack
/// in a record and each can silently corrupt the ones under it, so every chunk shape here is
/// asserted byte-identical after a round trip:
///
/// predictive-Z — a directional-Z array that matches its prediction is omitted entirely,
/// compression — each record deflates independently, or stores raw when that doesn't shrink it,
/// compact index — the trailer carries no file offsets; the reader sums record lengths instead.
///
/// Several tests assert on file size, because a round trip alone cannot tell you a transform ran:
/// an encoder that elided nothing and compressed nothing would still round-trip perfectly.
///
[Collection("Sequential Pathfinding Tests")]
public class StepCacheFileFormatTests
{
// ---- chunk builders ----
///
/// Per-cell varying masks and Zs. Nothing about it is uniform or predictable, so it exercises
/// the Full record with residual arrays present.
///
private static StepChunk VariedChunk(int seed = 0)
{
var c = new StepChunk();
for (var i = 0; i < StepChunk.CellsPerChunk; i++)
{
c.WalkMask[i] = (byte)((i + seed) & 0xFF);
c.WetMask[i] = (byte)((i * 7 + seed) & 0xFF);
c.SourceZ[i] = (sbyte)((i + seed) % 40 - 20);
c.WalkZN[i] = (sbyte)(c.SourceZ[i] + i % 3);
c.SwimZS[i] = (sbyte)(c.SourceZ[i] - i % 2);
}
return c;
}
/// Every cell identical — the Uniform record, ~28 bytes on disk.
private static StepChunk UniformChunk(sbyte z = 10)
{
var c = new StepChunk();
Array.Fill(c.WalkMask, (byte)0xC1);
Array.Fill(c.SourceZ, z);
foreach (var arr in AllBaseZArrays(c))
{
Array.Fill(arr, z);
}
return c;
}
///
/// Flat terrain, but NOT uniform: masks and SourceZ vary per cell while every directional Z
/// equals its masked prediction. That is the exact shape predictive-Z is built for, so all 16
/// arrays must elide. It doubles as the coastline case — partial walkability, non-zero SourceZ,
/// and 0 in every blocked direction, which is where a naive (unmasked) predictor would emit a
/// -SourceZ residual on every blocked direction and elide nothing.
///
private static StepChunk FlatFullChunk(sbyte baseZ = 10)
{
var c = new StepChunk();
for (var i = 0; i < StepChunk.CellsPerChunk; i++)
{
c.WalkMask[i] = (byte)(i & 0xFF);
c.WetMask[i] = (byte)(~i & 0xFF);
c.SourceZ[i] = (sbyte)(baseZ + i % 7 - 3);
}
var walk = new[] { c.WalkZN, c.WalkZNE, c.WalkZE, c.WalkZSE, c.WalkZS, c.WalkZSW, c.WalkZW, c.WalkZNW };
var swim = new[] { c.SwimZN, c.SwimZNE, c.SwimZE, c.SwimZSE, c.SwimZS, c.SwimZSW, c.SwimZW, c.SwimZNW };
for (var i = 0; i < StepChunk.CellsPerChunk; i++)
{
for (var b = 0; b < 8; b++)
{
walk[b][i] = (sbyte)((c.WalkMask[i] >> b & 1) != 0 ? c.SourceZ[i] : 0);
swim[b][i] = (sbyte)((c.WetMask[i] >> b & 1) != 0 ? c.SourceZ[i] : 0);
}
}
return c;
}
private static StepChunk WithSwimLayer(StepChunk c)
{
c.AllocateSwimLayer();
for (var i = 0; i < StepChunk.CellsPerChunk; i++)
{
c.SwimSourceZ[i] = (sbyte)(i % 30 - 15);
c.SwimMask[i] = (byte)(i * 5 & 0xFF);
c.SwimZN_Layer[i] = (sbyte)(i % 7);
c.SwimZNW_Layer[i] = (sbyte)-(i % 4);
}
return c;
}
private static StepChunk WithStrataAt(StepChunk c, int cell)
{
var offsets = new ushort[StepChunk.CellsPerChunk];
Array.Fill(offsets, StepChunk.NoStrata);
offsets[cell] = 0;
var data = new byte[1 + StepChunk.StratumByteLength];
data[0] = 1;
c.SetStrata(offsets, data);
return c;
}
private static sbyte[][] AllBaseZArrays(StepChunk c) =>
[
c.WalkZN, c.WalkZNE, c.WalkZE, c.WalkZSE, c.WalkZS, c.WalkZSW, c.WalkZW, c.WalkZNW,
c.SwimZN, c.SwimZNE, c.SwimZE, c.SwimZSE, c.SwimZS, c.SwimZSW, c.SwimZW, c.SwimZNW
];
private static sbyte[][] AllSwimLayerArrays(StepChunk c) =>
[
c.SwimZN_Layer, c.SwimZNE_Layer, c.SwimZE_Layer, c.SwimZSE_Layer,
c.SwimZS_Layer, c.SwimZSW_Layer, c.SwimZW_Layer, c.SwimZNW_Layer
];
// ---- round-trip plumbing ----
private static string Write(params (int cx, int cy, StepChunk c)[] chunks)
{
var path = Path.Combine(Path.GetTempPath(), $"swb_{Guid.NewGuid():N}.swb");
StepCacheFile.Write(path, 1u, chunks);
return path;
}
private static StepChunk RoundTrip(StepChunk src, out long fileLength, int cx = 3, int cy = 4)
{
var path = Write((cx, cy, src));
try
{
fileLength = new FileInfo(path).Length;
using var reader = StepCacheFile.OpenForLazy(path);
Assert.NotNull(reader);
var rt = reader!.TryReadChunk(cx, cy);
Assert.NotNull(rt);
return rt!;
}
finally
{
File.Delete(path);
}
}
private static StepChunk RoundTrip(StepChunk src) => RoundTrip(src, out _);
private static void AssertIdentical(StepChunk expected, StepChunk actual)
{
Assert.True(expected.WalkMask.AsSpan().SequenceEqual(actual.WalkMask), "WalkMask differs");
Assert.True(expected.WetMask.AsSpan().SequenceEqual(actual.WetMask), "WetMask differs");
Assert.True(expected.SourceZ.AsSpan().SequenceEqual(actual.SourceZ), "SourceZ differs");
var ez = AllBaseZArrays(expected);
var az = AllBaseZArrays(actual);
for (var i = 0; i < ez.Length; i++)
{
Assert.True(ez[i].AsSpan().SequenceEqual(az[i]), $"base Z array {i} differs");
}
Assert.Equal(expected.HasSwimLayer, actual.HasSwimLayer);
if (expected.HasSwimLayer)
{
Assert.True(expected.SwimSourceZ.AsSpan().SequenceEqual(actual.SwimSourceZ), "SwimSourceZ differs");
Assert.True(expected.SwimMask.AsSpan().SequenceEqual(actual.SwimMask), "SwimMask differs");
var el = AllSwimLayerArrays(expected);
var al = AllSwimLayerArrays(actual);
for (var i = 0; i < el.Length; i++)
{
Assert.True(el[i].AsSpan().SequenceEqual(al[i]), $"swim-layer Z array {i} differs");
}
}
Assert.True(expected.StrataData.SequenceEqual(actual.StrataData), "StrataData differs");
}
// ---- predictive-Z transform ----
[Theory]
[InlineData((sbyte)0, (sbyte)0)]
[InlineData((sbyte)10, (sbyte)10)]
[InlineData((sbyte)0, (sbyte)10)]
[InlineData((sbyte)10, (sbyte)0)]
[InlineData((sbyte)-20, (sbyte)15)]
[InlineData(sbyte.MinValue, sbyte.MaxValue)]
[InlineData(sbyte.MaxValue, sbyte.MinValue)]
[InlineData(sbyte.MinValue, (sbyte)1)]
[InlineData((sbyte)127, (sbyte)-1)]
public void Residual_RoundTripsLosslessly_AcrossTheFullSByteRange(sbyte z, sbyte predict)
{
var residual = StepCacheFile.EncodeResidual(z, predict);
Assert.Equal(z, StepCacheFile.DecodeZ(predict, residual));
}
[Theory]
[InlineData((byte)0b0000_0001, 0, (sbyte)42, (sbyte)42)] // passable -> predict SourceZ
[InlineData((byte)0b0000_0000, 0, (sbyte)42, (sbyte)0)] // blocked -> predict 0
[InlineData((byte)0b1000_0000, 7, (sbyte)-13, (sbyte)-13)]
[InlineData((byte)0b0111_1111, 7, (sbyte)-13, (sbyte)0)]
public void Predict_IsSourceZWherePassable_ZeroWhereBlocked(byte maskByte, int bit, sbyte sourceZ, sbyte expected)
{
Assert.Equal(expected, StepCacheFile.Predict(maskByte, bit, sourceZ));
}
[Fact]
public void FlatChunk_ElidesEveryZArray()
{
var src = FlatFullChunk();
var rt = RoundTrip(src, out var fileLength);
AssertIdentical(src, rt);
// A Full record carrying all 16 Z arrays runs past 5 KB. Landing under 1100 bytes is only
// possible if every one of them elided.
Assert.True(fileLength < 1100, $"expected every base Z array to elide; file was {fileLength} bytes");
}
[Fact]
public void CoastlineChunk_ElidesEveryZArray()
{
// Partial walkability with a non-zero SourceZ: the shape that defeats an unmasked predictor.
var src = FlatFullChunk(baseZ: 25);
var rt = RoundTrip(src, out var fileLength);
AssertIdentical(src, rt);
Assert.True(fileLength < 1100, $"masked predictor should elide every array; file was {fileLength} bytes");
}
[Fact]
public void SlopeInOneDirection_StoresOnlyThatZArray()
{
var flat = FlatFullChunk();
RoundTrip(flat, out var flatLength);
// Raise WalkZN on cells walkable to the north. Exactly one array (WalkZN) now disagrees
// with its prediction; the other 15 must still elide.
var sloped = FlatFullChunk();
for (var i = 0; i < StepChunk.CellsPerChunk; i++)
{
if ((sloped.WalkMask[i] & 1) != 0)
{
sloped.WalkZN[i]++;
}
}
var rt = RoundTrip(sloped, out var slopedLength);
AssertIdentical(sloped, rt);
Assert.True(slopedLength > flatLength, "a present Z array should grow the record");
Assert.True(
slopedLength <= flatLength + StepChunk.CellsPerChunk,
$"only one 256-byte residual array should have been added; grew by {slopedLength - flatLength}"
);
}
// ---- compression ----
[Fact]
public void VariedChunk_Compresses_AndRoundTrips()
{
var src = VariedChunk(seed: 4);
var rt = RoundTrip(src, out var fileLength);
AssertIdentical(src, rt);
// The uncompressed Full record for a varied chunk exceeds 5 KB.
Assert.True(fileLength < 4000, $"expected compression to shrink the record; file was {fileLength} bytes");
}
[Fact]
public void UniformChunk_StoredRaw_RoundTrips()
{
// A Uniform body is ~28 bytes and deflate cannot shrink it, so the writer stores it raw and
// the reader has to notice that from the payload length alone.
var src = UniformChunk(z: 12);
var rt = RoundTrip(src, out var fileLength);
AssertIdentical(src, rt);
Assert.True(fileLength < 200, $"uniform record should stay tiny; file was {fileLength} bytes");
}
// ---- optional trailers ----
[Fact]
public void SwimLayer_RoundTrips() => AssertIdentical(
WithSwimLayer(VariedChunk()),
RoundTrip(WithSwimLayer(VariedChunk()))
);
[Fact]
public void Strata_RoundTrips()
{
var src = WithStrataAt(VariedChunk(), cell: 10);
var rt = RoundTrip(src);
AssertIdentical(src, rt);
Assert.True(rt.IsCellMultiZ(10));
}
[Fact]
public void SwimLayerAndStrata_RoundTripTogether()
{
// Both trailers present at once, which is the only case that pins their relative order.
var src = WithStrataAt(WithSwimLayer(VariedChunk()), cell: 20);
var rt = RoundTrip(src);
AssertIdentical(src, rt);
Assert.True(rt.HasSwimLayer);
Assert.True(rt.IsCellMultiZ(20));
}
// ---- compact index ----
[Fact]
public void MultipleChunks_ResolveIndividually_FromDerivedOffsets()
{
// The index stores no offsets, so a reader that mis-sums record lengths would hand back a
// neighbouring chunk's bytes. Distinct content per coordinate is what catches that. The mix
// of record sizes matters: a raw-stored Uniform sits between two compressed Full records,
// and one coordinate is large enough to exercise the packed key's high 16 bits.
var chunks = new List<(int cx, int cy, StepChunk c)>
{
(1, 1, VariedChunk(seed: 3)),
(2, 5, UniformChunk(z: 14)),
(10, 3, VariedChunk(seed: 99)),
(300, 200, VariedChunk(seed: 17))
};
var path = Write(chunks.ToArray());
try
{
using var reader = StepCacheFile.OpenForLazy(path);
Assert.NotNull(reader);
Assert.Equal((uint)chunks.Count, reader!.ChunkCount);
foreach (var (cx, cy, src) in chunks)
{
Assert.True(reader.Has(cx, cy), $"missing chunk ({cx},{cy})");
var rt = reader.TryReadChunk(cx, cy);
Assert.NotNull(rt);
AssertIdentical(src, rt!);
}
Assert.Null(reader.TryReadChunk(7, 7)); // never written
}
finally
{
File.Delete(path);
}
}
[Fact]
public void EmptyChunkSet_WritesAReadableFile()
{
var path = Write();
try
{
using var reader = StepCacheFile.OpenForLazy(path);
Assert.NotNull(reader);
Assert.Equal(0u, reader!.ChunkCount);
Assert.Null(reader.TryReadChunk(0, 0));
}
finally
{
File.Delete(path);
}
}
// ---- version gate ----
[Theory]
[InlineData(0u)]
[InlineData(5u)]
[InlineData(8u)]
[InlineData(StepCacheFile.FormatVersion + 1)]
public void UnsupportedVersion_IsRejected(uint version)
{
var path = Write((0, 0, UniformChunk()));
try
{
var bytes = File.ReadAllBytes(path);
BitConverter.GetBytes(version).CopyTo(bytes, 4); // Version sits right after Magic
File.WriteAllBytes(path, bytes);
Assert.Null(StepCacheFile.OpenForLazy(path));
}
finally
{
File.Delete(path);
}
}
}