feat(content): bake and read flat collision assets

Append strict collision/topology payloads to the existing prepared package so later physics cutover can drop parsed DAT graphs without adding a second mapping or changing traversal behavior. The full 2,232,170-key catalog is deterministic across worker counts, exact-byte aliased, corruption-isolated, and cancellation-safe.
This commit is contained in:
Erik 2026-07-25 15:22:08 +02:00
parent d9300c7854
commit 9cd42417a8
29 changed files with 2868 additions and 63 deletions

View file

@ -76,6 +76,43 @@ public sealed class BakeDeterminismTests : IDisposable {
$"pak sizes differ between runs: {bytesA.Length} vs {bytesB.Length} bytes");
Assert.True(bytesA.AsSpan().SequenceEqual(bytesB),
"two bakes of the same id set produced different bytes — the sorted-batching determinism guarantee is broken");
using var reader = new PakReader(pathA);
foreach (uint gfxId in new[]
{
0x01000001u,
0x010002B4u,
0x010008A8u,
0x010014C3u,
})
{
Assert.True(reader.ContainsKey(PakKey.Compose(
PakAssetType.GfxObjCollision,
gfxId)));
}
foreach (uint setupId in new[]
{
0x02000001u,
0x020019FFu,
0x020005D8u,
})
{
Assert.True(reader.ContainsKey(PakKey.Compose(
PakAssetType.SetupCollision,
setupId)));
}
foreach (uint cellId in new[]
{
0xA9B40100u,
0xA9B40101u,
})
{
Assert.True(reader.ContainsKey(PakKey.Compose(
PakAssetType.CellStructureCollision,
cellId)));
Assert.True(reader.ContainsKey(PakKey.Compose(
PakAssetType.EnvCellTopology,
cellId)));
}
}
[Fact]
@ -111,7 +148,11 @@ public sealed class BakeDeterminismTests : IDisposable {
Assert.Equal(8, reportA.EnvCellKeys);
Assert.Equal(1, reportA.UniqueEnvCellGeometries);
Assert.Equal(7, reportA.EnvCellAliases);
Assert.Equal(1, reportA.PhysicalBlobs);
Assert.Equal(8, reportA.CellStructureCollisionKeys);
Assert.Equal(1, reportA.UniqueCellStructureCollisions);
Assert.Equal(7, reportA.CellStructureCollisionAliases);
Assert.Equal(8, reportA.EnvCellTopologyKeys);
Assert.Equal(10, reportA.PhysicalBlobs);
Assert.Equal(reportA.TotalKeys, reportB.TotalKeys);
Assert.True(File.ReadAllBytes(pathA).AsSpan().SequenceEqual(File.ReadAllBytes(pathB)));
@ -124,6 +165,27 @@ public sealed class BakeDeterminismTests : IDisposable {
Assert.All(receipts, receipt => Assert.Equal(receipts[0].Length, receipt.Length));
Assert.All(receipts, receipt => Assert.Equal(receipts[0].Crc32, receipt.Crc32));
var collisionReceipts = ids
.Select(id => reader.GetTocEntryForTest(
PakKey.Compose(PakAssetType.CellStructureCollision, id)))
.ToArray();
Assert.All(
collisionReceipts,
receipt => Assert.Equal(
collisionReceipts[0].Offset,
receipt.Offset));
Assert.All(
collisionReceipts,
receipt => Assert.Equal(
collisionReceipts[0].Length,
receipt.Length));
Assert.Equal(
ids.Count,
ids.Select(id => reader.GetTocEntryForTest(
PakKey.Compose(PakAssetType.EnvCellTopology, id)).Offset)
.Distinct()
.Count());
Assert.True(
reader.TryReadObjectMeshData(
PakKey.Compose(PakAssetType.EnvCellMesh, ids.Min()),
@ -132,4 +194,34 @@ public sealed class BakeDeterminismTests : IDisposable {
Assert.Equal(0xE000_0000_0000_0000UL,
geometry.ObjectId & 0xF000_0000_0000_0000UL);
}
[Fact]
public void Bake_CancellationCannotReplaceLastGoodPackage()
{
string? datDir = ResolveDatDir();
if (datDir is null)
return;
string destination = NewTempPakPath();
byte[] lastGood = [0x41, 0x43, 0x50, 0x4B, 1, 2, 3, 4];
File.WriteAllBytes(destination, lastGood);
using var cancellation = new CancellationTokenSource();
cancellation.CancelAfter(TimeSpan.FromMilliseconds(20));
Assert.Throws<OperationCanceledException>(
() => BakeRunner.RunDetailed(
new BakeOptions
{
DatDir = datDir,
OutPath = destination,
Threads = 2,
CancellationToken = cancellation.Token,
}));
Assert.Equal(lastGood, File.ReadAllBytes(destination));
Assert.Empty(
Directory.EnumerateFiles(
Path.GetDirectoryName(destination)!,
$".{Path.GetFileName(destination)}.*.tmp"));
}
}