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:
parent
d9300c7854
commit
9cd42417a8
29 changed files with 2868 additions and 63 deletions
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,6 +154,33 @@ public sealed class BakeOutputTransactionTests : IDisposable
|
|||
() => BakeArtifactValidator.Validate(pak, expected, expectedTocCount: 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArtifactValidator_RejectsIncompleteTypedCatalog()
|
||||
{
|
||||
string pak = Path.Combine(_directory, "typed-content.pak");
|
||||
var expected = Header();
|
||||
using (var writer = new PakWriter(pak, expected))
|
||||
{
|
||||
writer.AddBlob(
|
||||
PakKey.Compose(PakAssetType.GfxObjMesh, 1),
|
||||
new ObjectMeshData { ObjectId = 1 });
|
||||
writer.Finish();
|
||||
}
|
||||
|
||||
var expectedTypes = new Dictionary<PakAssetType, int>
|
||||
{
|
||||
[PakAssetType.GfxObjMesh] = 1,
|
||||
[PakAssetType.GfxObjCollision] = 1,
|
||||
};
|
||||
InvalidDataException exception = Assert.Throws<InvalidDataException>(
|
||||
() => BakeArtifactValidator.Validate(
|
||||
pak,
|
||||
expected,
|
||||
expectedTocCount: 1,
|
||||
expectedTypeCounts: expectedTypes));
|
||||
Assert.Contains("GfxObjCollision", exception.Message);
|
||||
}
|
||||
|
||||
private PakHeader Header() =>
|
||||
new()
|
||||
{
|
||||
|
|
|
|||
24
tests/AcDream.Bake.Tests/ExactPayloadAliasCatalogTests.cs
Normal file
24
tests/AcDream.Bake.Tests/ExactPayloadAliasCatalogTests.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
namespace AcDream.Bake.Tests;
|
||||
|
||||
public sealed class ExactPayloadAliasCatalogTests
|
||||
{
|
||||
[Fact]
|
||||
public void AliasRequiresCompleteByteEquality()
|
||||
{
|
||||
var catalog = new ExactPayloadAliasCatalog();
|
||||
byte[] primary = [1, 2, 3, 4, 5];
|
||||
catalog.Add(0x1234UL, primary);
|
||||
|
||||
Assert.True(catalog.TryFind(
|
||||
[1, 2, 3, 4, 5],
|
||||
out ulong primaryKey));
|
||||
Assert.Equal(0x1234UL, primaryKey);
|
||||
|
||||
Assert.False(catalog.TryFind(
|
||||
[1, 2, 3, 4, 4],
|
||||
out _));
|
||||
Assert.False(catalog.TryFind(
|
||||
[1, 2, 3, 4, 5, 0],
|
||||
out _));
|
||||
}
|
||||
}
|
||||
512
tests/AcDream.Content.Tests/FlatCollisionAssetSerializerTests.cs
Normal file
512
tests/AcDream.Content.Tests/FlatCollisionAssetSerializerTests.cs
Normal file
|
|
@ -0,0 +1,512 @@
|
|||
using System.Collections.Immutable;
|
||||
using System.Numerics;
|
||||
using AcDream.Content.Pak;
|
||||
using AcDream.Core.Physics;
|
||||
using DatReaderWriter.Enums;
|
||||
|
||||
namespace AcDream.Content.Tests;
|
||||
|
||||
public sealed class FlatCollisionAssetSerializerTests : IDisposable
|
||||
{
|
||||
private static readonly PreparedAssetCatalogIdentity Identity =
|
||||
new(10, 20, 30, 40, PakFormat.CurrentBakeToolVersion);
|
||||
|
||||
private readonly List<string> _paths = [];
|
||||
|
||||
[Fact]
|
||||
public void EveryPayload_RoundTripsToByteIdenticalRepresentation()
|
||||
{
|
||||
FlatGfxObjCollisionAsset gfx = GfxAsset();
|
||||
FlatSetupCollision setup = SetupAsset();
|
||||
FlatCellStructureCollisionAsset structure = CellStructureAsset();
|
||||
FlatEnvCellTopology topology = TopologyAsset();
|
||||
|
||||
byte[] gfxBytes = FlatCollisionAssetSerializer.Serialize(gfx);
|
||||
byte[] setupBytes = FlatCollisionAssetSerializer.Serialize(setup);
|
||||
byte[] structureBytes =
|
||||
FlatCollisionAssetSerializer.Serialize(structure);
|
||||
byte[] topologyBytes =
|
||||
FlatCollisionAssetSerializer.Serialize(topology);
|
||||
|
||||
FlatGfxObjCollisionAsset readGfx =
|
||||
FlatCollisionAssetSerializer.DeserializeGfxObj(gfxBytes);
|
||||
FlatSetupCollision readSetup =
|
||||
FlatCollisionAssetSerializer.DeserializeSetup(setupBytes);
|
||||
FlatCellStructureCollisionAsset readStructure =
|
||||
FlatCollisionAssetSerializer.DeserializeCellStructure(
|
||||
structureBytes);
|
||||
FlatEnvCellTopology readTopology =
|
||||
FlatCollisionAssetSerializer.DeserializeEnvCellTopology(
|
||||
topologyBytes);
|
||||
|
||||
Assert.Equal(
|
||||
gfxBytes,
|
||||
FlatCollisionAssetSerializer.Serialize(readGfx));
|
||||
Assert.Equal(
|
||||
setupBytes,
|
||||
FlatCollisionAssetSerializer.Serialize(readSetup));
|
||||
Assert.Equal(
|
||||
structureBytes,
|
||||
FlatCollisionAssetSerializer.Serialize(readStructure));
|
||||
Assert.Equal(
|
||||
topologyBytes,
|
||||
FlatCollisionAssetSerializer.Serialize(readTopology));
|
||||
|
||||
AssertFloatBits(
|
||||
gfx.VisualBounds!.Value.Radius,
|
||||
readGfx.VisualBounds!.Value.Radius);
|
||||
AssertFloatBits(
|
||||
setup.StepDownHeight,
|
||||
readSetup.StepDownHeight);
|
||||
AssertFloatBits(
|
||||
structure.PhysicsBsp.Nodes[0].SplittingPlane.D,
|
||||
readStructure.PhysicsBsp.Nodes[0].SplittingPlane.D);
|
||||
Assert.Equal(
|
||||
topology.VisibleCellIds.AsEnumerable(),
|
||||
readTopology.VisibleCellIds.AsEnumerable());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Encoding_IsLittleEndianAndPreservesSpecialFloatBits()
|
||||
{
|
||||
float special = BitConverter.Int32BitsToSingle(
|
||||
unchecked((int)0xFFC0_1234u));
|
||||
var setup = new FlatSetupCollision(
|
||||
ImmutableArray<FlatCollisionCylinder>.Empty,
|
||||
ImmutableArray<FlatCollisionSphere>.Empty,
|
||||
special,
|
||||
-0f,
|
||||
float.PositiveInfinity,
|
||||
float.NegativeInfinity);
|
||||
|
||||
byte[] bytes = FlatCollisionAssetSerializer.Serialize(setup);
|
||||
|
||||
// Header is "ACCL", kind=Setup, schema=1, reserved=0. Counts follow,
|
||||
// then the exact little-endian IEEE-754 bits for Height.
|
||||
Assert.Equal(
|
||||
new byte[]
|
||||
{
|
||||
0x41, 0x43, 0x43, 0x4C,
|
||||
0x02, 0x01, 0x00, 0x00,
|
||||
},
|
||||
bytes[..8]);
|
||||
Assert.Equal(
|
||||
new byte[] { 0x34, 0x12, 0xC0, 0xFF },
|
||||
bytes[16..20]);
|
||||
|
||||
FlatSetupCollision read =
|
||||
FlatCollisionAssetSerializer.DeserializeSetup(bytes);
|
||||
AssertFloatBits(special, read.Height);
|
||||
AssertFloatBits(-0f, read.Radius);
|
||||
AssertFloatBits(float.PositiveInfinity, read.StepUpHeight);
|
||||
AssertFloatBits(float.NegativeInfinity, read.StepDownHeight);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Readers_RejectWrongKindTruncationTrailingAndInvalidCounts()
|
||||
{
|
||||
byte[] setup = FlatCollisionAssetSerializer.Serialize(SetupAsset());
|
||||
|
||||
Assert.Throws<InvalidDataException>(
|
||||
() => FlatCollisionAssetSerializer.DeserializeGfxObj(setup));
|
||||
|
||||
for (int length = 0; length < setup.Length; length++)
|
||||
{
|
||||
Assert.ThrowsAny<Exception>(
|
||||
() => FlatCollisionAssetSerializer.DeserializeSetup(
|
||||
setup.AsSpan(0, length)));
|
||||
}
|
||||
|
||||
Assert.Throws<InvalidDataException>(
|
||||
() => FlatCollisionAssetSerializer.DeserializeSetup(
|
||||
[.. setup, 0x00]));
|
||||
|
||||
byte[] negativeCount = (byte[])setup.Clone();
|
||||
negativeCount[8] = 0xFF;
|
||||
negativeCount[9] = 0xFF;
|
||||
negativeCount[10] = 0xFF;
|
||||
negativeCount[11] = 0xFF;
|
||||
Assert.Throws<InvalidDataException>(
|
||||
() => FlatCollisionAssetSerializer.DeserializeSetup(
|
||||
negativeCount));
|
||||
|
||||
byte[] impossibleCombinedCounts = (byte[])setup.Clone();
|
||||
impossibleCombinedCounts[8] = 0x03;
|
||||
impossibleCombinedCounts[12] = 0x03;
|
||||
Assert.Throws<InvalidDataException>(
|
||||
() => FlatCollisionAssetSerializer.DeserializeSetup(
|
||||
impossibleCombinedCounts));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SerializeAndDeserialize_PropagatePreCancellation()
|
||||
{
|
||||
using var cancellation = new CancellationTokenSource();
|
||||
cancellation.Cancel();
|
||||
|
||||
Assert.Throws<OperationCanceledException>(
|
||||
() => FlatCollisionAssetSerializer.Serialize(
|
||||
GfxAsset(),
|
||||
cancellation.Token));
|
||||
byte[] bytes =
|
||||
FlatCollisionAssetSerializer.Serialize(GfxAsset());
|
||||
Assert.Throws<OperationCanceledException>(
|
||||
() => FlatCollisionAssetSerializer.DeserializeGfxObj(
|
||||
bytes,
|
||||
cancellation.Token));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreparedSource_ReadsAllCollisionKindsThroughSamePackageOwner()
|
||||
{
|
||||
const uint gfxId = 0x0100_0001u;
|
||||
const uint setupId = 0x0200_0001u;
|
||||
const uint cellId = 0xA9B4_0101u;
|
||||
string path = NewPath();
|
||||
using (var writer = new PakWriter(path, Header()))
|
||||
{
|
||||
writer.AddBlob(
|
||||
PakKey.Compose(PakAssetType.GfxObjCollision, gfxId),
|
||||
FlatCollisionAssetSerializer.Serialize(GfxAsset()));
|
||||
writer.AddBlob(
|
||||
PakKey.Compose(PakAssetType.SetupCollision, setupId),
|
||||
FlatCollisionAssetSerializer.Serialize(SetupAsset()));
|
||||
writer.AddBlob(
|
||||
PakKey.Compose(
|
||||
PakAssetType.CellStructureCollision,
|
||||
cellId),
|
||||
FlatCollisionAssetSerializer.Serialize(CellStructureAsset()));
|
||||
writer.AddBlob(
|
||||
PakKey.Compose(PakAssetType.EnvCellTopology, cellId),
|
||||
FlatCollisionAssetSerializer.Serialize(TopologyAsset()));
|
||||
writer.Finish();
|
||||
}
|
||||
|
||||
using var owner = new PakPreparedAssetSource(path, Identity);
|
||||
IPreparedAssetSource renderSource = owner;
|
||||
IPreparedCollisionSource collisionSource = owner;
|
||||
|
||||
Assert.Equal(
|
||||
PreparedAssetPresence.Available,
|
||||
collisionSource.ProbeCollision(
|
||||
PakAssetType.GfxObjCollision,
|
||||
gfxId));
|
||||
Assert.Equal(
|
||||
PreparedAssetReadStatus.Loaded,
|
||||
collisionSource.ReadGfxObjCollision(gfxId).Status);
|
||||
Assert.Equal(
|
||||
PreparedAssetReadStatus.Loaded,
|
||||
collisionSource.ReadSetupCollision(setupId).Status);
|
||||
Assert.Equal(
|
||||
PreparedAssetReadStatus.Loaded,
|
||||
collisionSource.ReadCellStructureCollision(cellId).Status);
|
||||
Assert.Equal(
|
||||
PreparedAssetReadStatus.Loaded,
|
||||
collisionSource.ReadEnvCellTopology(cellId).Status);
|
||||
Assert.Equal(4, collisionSource.CollisionStats.Loaded);
|
||||
Assert.Equal(0, renderSource.Stats.Reads);
|
||||
Assert.Equal(new FileInfo(path).Length, renderSource.MappedVirtualBytes);
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() => renderSource.Probe(
|
||||
PakAssetType.GfxObjCollision,
|
||||
gfxId));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() => collisionSource.ProbeCollision(
|
||||
PakAssetType.GfxObjMesh,
|
||||
gfxId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreparedSource_DistinguishesMissingCrcAndSchemaCorruption()
|
||||
{
|
||||
const uint setupId = 0x0200_0001u;
|
||||
string missingPath = NewPath();
|
||||
using (var writer = new PakWriter(missingPath, Header()))
|
||||
writer.Finish();
|
||||
|
||||
using (var source = new PakPreparedAssetSource(
|
||||
missingPath,
|
||||
Identity))
|
||||
{
|
||||
Assert.Equal(
|
||||
PreparedAssetReadStatus.Missing,
|
||||
source.ReadSetupCollision(setupId).Status);
|
||||
}
|
||||
|
||||
string schemaPath = NewPath();
|
||||
byte[] malformed =
|
||||
[.. FlatCollisionAssetSerializer.Serialize(SetupAsset()), 0x7F];
|
||||
using (var writer = new PakWriter(schemaPath, Header()))
|
||||
{
|
||||
writer.AddBlob(
|
||||
PakKey.Compose(PakAssetType.SetupCollision, setupId),
|
||||
malformed);
|
||||
writer.Finish();
|
||||
}
|
||||
|
||||
using (var source = new PakPreparedAssetSource(schemaPath, Identity))
|
||||
{
|
||||
Assert.Equal(
|
||||
PreparedAssetReadStatus.Corrupt,
|
||||
source.ReadSetupCollision(setupId).Status);
|
||||
Assert.Equal(
|
||||
PreparedAssetPresence.Corrupt,
|
||||
source.ProbeCollision(
|
||||
PakAssetType.SetupCollision,
|
||||
setupId));
|
||||
Assert.Equal(
|
||||
PreparedAssetReadStatus.Corrupt,
|
||||
source.ReadSetupCollision(setupId).Status);
|
||||
}
|
||||
|
||||
string crcPath = NewPath();
|
||||
using (var writer = new PakWriter(crcPath, Header()))
|
||||
{
|
||||
writer.AddBlob(
|
||||
PakKey.Compose(PakAssetType.SetupCollision, setupId),
|
||||
FlatCollisionAssetSerializer.Serialize(SetupAsset()));
|
||||
writer.Finish();
|
||||
}
|
||||
FlipFirstBlobByte(crcPath);
|
||||
using var corruptSource =
|
||||
new PakPreparedAssetSource(crcPath, Identity);
|
||||
Assert.Equal(
|
||||
PreparedAssetReadStatus.Corrupt,
|
||||
corruptSource.ReadSetupCollision(setupId).Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreparedSource_ConcurrentReadsRemainIndependentAndComplete()
|
||||
{
|
||||
const uint setupId = 0x0200_0001u;
|
||||
string path = NewPath();
|
||||
using (var writer = new PakWriter(path, Header()))
|
||||
{
|
||||
writer.AddBlob(
|
||||
PakKey.Compose(PakAssetType.SetupCollision, setupId),
|
||||
FlatCollisionAssetSerializer.Serialize(SetupAsset()));
|
||||
writer.Finish();
|
||||
}
|
||||
|
||||
using var source = new PakPreparedAssetSource(path, Identity);
|
||||
Parallel.For(
|
||||
0,
|
||||
64,
|
||||
_ =>
|
||||
{
|
||||
PreparedCollisionReadResult<FlatSetupCollision> result =
|
||||
source.ReadSetupCollision(setupId);
|
||||
Assert.Equal(PreparedAssetReadStatus.Loaded, result.Status);
|
||||
Assert.NotNull(result.Data);
|
||||
});
|
||||
|
||||
Assert.Equal(64, source.CollisionStats.Reads);
|
||||
Assert.Equal(64, source.CollisionStats.Loaded);
|
||||
Assert.Equal(0, source.CollisionStats.Corrupt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RenderDecoder_CannotPoisonCollisionTypedEntry()
|
||||
{
|
||||
const uint setupId = 0x0200_0001u;
|
||||
string path = NewPath();
|
||||
ulong key = PakKey.Compose(
|
||||
PakAssetType.SetupCollision,
|
||||
setupId);
|
||||
using (var writer = new PakWriter(path, Header()))
|
||||
{
|
||||
writer.AddBlob(
|
||||
key,
|
||||
FlatCollisionAssetSerializer.Serialize(SetupAsset()));
|
||||
writer.Finish();
|
||||
}
|
||||
|
||||
using (var reader = new PakReader(path))
|
||||
{
|
||||
Assert.Equal(
|
||||
PakObjectReadStatus.Missing,
|
||||
reader.ReadObjectMeshData(key, out ObjectMeshData? mesh));
|
||||
Assert.Null(mesh);
|
||||
Assert.Equal(PakEntryState.Available, reader.ProbeEntry(key));
|
||||
}
|
||||
|
||||
using var source = new PakPreparedAssetSource(path, Identity);
|
||||
Assert.Equal(
|
||||
PreparedAssetReadStatus.Loaded,
|
||||
source.ReadSetupCollision(setupId).Status);
|
||||
}
|
||||
|
||||
private static FlatGfxObjCollisionAsset GfxAsset() =>
|
||||
new(
|
||||
PhysicsBsp(),
|
||||
new FlatCollisionSphere(
|
||||
new Vector3(Float(0x3F80_0001), -0f, 3f),
|
||||
Float(0x7FC0_1234)),
|
||||
new FlatGfxObjVisualBounds(
|
||||
new Vector3(-1, -2, -3),
|
||||
new Vector3(4, 5, 6),
|
||||
new Vector3(1.5f),
|
||||
Float(0x4123_4567),
|
||||
new Vector3(2.5f, 3.5f, 4.5f)));
|
||||
|
||||
private static FlatSetupCollision SetupAsset() =>
|
||||
new(
|
||||
ImmutableArray.Create(
|
||||
new FlatCollisionCylinder(
|
||||
new Vector3(1, 2, 3),
|
||||
Float(0x3F00_0001),
|
||||
Float(0x4000_0001))),
|
||||
ImmutableArray.Create(
|
||||
new FlatCollisionSphere(
|
||||
new Vector3(-1, -2, -3),
|
||||
Float(0x4040_0001))),
|
||||
Float(0x4080_0001),
|
||||
Float(0x40A0_0001),
|
||||
Float(0x40C0_0001),
|
||||
Float(0x40E0_0001));
|
||||
|
||||
private static FlatCellStructureCollisionAsset CellStructureAsset() =>
|
||||
new(
|
||||
PhysicsBsp(),
|
||||
new FlatCellContainmentBsp(
|
||||
0,
|
||||
ImmutableArray.Create(
|
||||
new FlatCellBspNode(
|
||||
BSPNodeType.BPIN,
|
||||
new Plane(Vector3.UnitX, -1),
|
||||
1,
|
||||
2,
|
||||
0),
|
||||
new FlatCellBspNode(
|
||||
BSPNodeType.Leaf,
|
||||
new Plane(Vector3.UnitY, -2),
|
||||
-1,
|
||||
-1,
|
||||
1),
|
||||
new FlatCellBspNode(
|
||||
BSPNodeType.Leaf,
|
||||
new Plane(Vector3.UnitZ, -3),
|
||||
-1,
|
||||
-1,
|
||||
2))),
|
||||
PolygonTable());
|
||||
|
||||
private static FlatEnvCellTopology TopologyAsset() =>
|
||||
new(
|
||||
ImmutableArray.Create(
|
||||
new FlatEnvCellPortal(0x0102, 3, 4, 0),
|
||||
new FlatEnvCellPortal(0x0103, 8, 2, 1)),
|
||||
ImmutableArray.Create(0xA9B4_0101u, 0xA9B4_0102u),
|
||||
true);
|
||||
|
||||
private static FlatPhysicsBsp PhysicsBsp() =>
|
||||
new(
|
||||
0,
|
||||
ImmutableArray.Create(
|
||||
Node(BSPNodeType.BPIN, 1, 2, 0, 0, 0),
|
||||
Node(BSPNodeType.Leaf, -1, -1, 1, 0, 1),
|
||||
Node(BSPNodeType.Leaf, -1, -1, 2, 1, 1)),
|
||||
ImmutableArray.Create(0, 1),
|
||||
PolygonTable());
|
||||
|
||||
private static FlatPhysicsBspNode Node(
|
||||
BSPNodeType type,
|
||||
int positive,
|
||||
int negative,
|
||||
int leaf,
|
||||
int polygonStart,
|
||||
int polygonCount) =>
|
||||
new(
|
||||
type,
|
||||
new Plane(
|
||||
new Vector3(1 + leaf, 2 + leaf, 3 + leaf),
|
||||
Float(0x3F80_0000u + (uint)leaf)),
|
||||
positive,
|
||||
negative,
|
||||
leaf,
|
||||
leaf & 1,
|
||||
new FlatCollisionSphere(
|
||||
new Vector3(4 + leaf, 5 + leaf, 6 + leaf),
|
||||
7 + leaf),
|
||||
new FlatIndexRange(polygonStart, polygonCount));
|
||||
|
||||
private static FlatPolygonTable PolygonTable() =>
|
||||
new(
|
||||
ImmutableArray.Create(
|
||||
new FlatCollisionPolygon(
|
||||
3,
|
||||
new Plane(Vector3.UnitX, -1),
|
||||
CullMode.Clockwise,
|
||||
3,
|
||||
new FlatIndexRange(0, 3)),
|
||||
new FlatCollisionPolygon(
|
||||
8,
|
||||
new Plane(Vector3.UnitY, -2),
|
||||
CullMode.CounterClockwise,
|
||||
3,
|
||||
new FlatIndexRange(3, 3))),
|
||||
ImmutableArray.Create(
|
||||
Vector3.Zero,
|
||||
Vector3.UnitY,
|
||||
Vector3.UnitZ,
|
||||
Vector3.One,
|
||||
Vector3.UnitX,
|
||||
Vector3.UnitZ));
|
||||
|
||||
private string NewPath()
|
||||
{
|
||||
string path = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
$"acdream-flat-collision-{Guid.NewGuid():N}.pak");
|
||||
_paths.Add(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
private static PakHeader Header() =>
|
||||
new()
|
||||
{
|
||||
PortalIteration = Identity.PortalIteration,
|
||||
CellIteration = Identity.CellIteration,
|
||||
HighResIteration = Identity.HighResIteration,
|
||||
LanguageIteration = Identity.LanguageIteration,
|
||||
BakeToolVersion = Identity.BakeToolVersion,
|
||||
};
|
||||
|
||||
private static void FlipFirstBlobByte(string path)
|
||||
{
|
||||
using var stream = new FileStream(
|
||||
path,
|
||||
FileMode.Open,
|
||||
FileAccess.ReadWrite,
|
||||
FileShare.None);
|
||||
stream.Position = PakHeader.Size + 4;
|
||||
int value = stream.ReadByte();
|
||||
Assert.NotEqual(-1, value);
|
||||
stream.Position = PakHeader.Size + 4;
|
||||
stream.WriteByte((byte)(value ^ 0xFF));
|
||||
}
|
||||
|
||||
private static float Float(uint bits) =>
|
||||
BitConverter.Int32BitsToSingle(unchecked((int)bits));
|
||||
|
||||
private static void AssertFloatBits(float expected, float actual) =>
|
||||
Assert.Equal(
|
||||
BitConverter.SingleToInt32Bits(expected),
|
||||
BitConverter.SingleToInt32Bits(actual));
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (string path in _paths)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best-effort cleanup.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
using AcDream.Content.Pak;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.Options;
|
||||
|
||||
namespace AcDream.Content.Tests;
|
||||
|
||||
public sealed class InstalledPreparedCollisionCatalogTests
|
||||
{
|
||||
[Fact]
|
||||
public void InstalledPackage_ContainsAndReadsCanonicalCollisionKeys()
|
||||
{
|
||||
string? datDir = ResolveDatDir();
|
||||
if (datDir is null)
|
||||
return;
|
||||
|
||||
string packagePath = Path.Combine(datDir, "acdream.pak");
|
||||
if (!File.Exists(packagePath))
|
||||
return;
|
||||
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
using var adapter = new DatCollectionAdapter(dats);
|
||||
using var source =
|
||||
new PakPreparedAssetSource(packagePath, adapter);
|
||||
|
||||
Assert.Equal(
|
||||
PreparedAssetReadStatus.Loaded,
|
||||
source.ReadGfxObjCollision(0x0100_0A2Bu).Status);
|
||||
Assert.Equal(
|
||||
PreparedAssetReadStatus.Loaded,
|
||||
source.ReadSetupCollision(0x0200_0001u).Status);
|
||||
Assert.Equal(
|
||||
PreparedAssetReadStatus.Loaded,
|
||||
source.ReadCellStructureCollision(0xA9B4_013Fu).Status);
|
||||
Assert.Equal(
|
||||
PreparedAssetReadStatus.Loaded,
|
||||
source.ReadEnvCellTopology(0xA9B4_013Fu).Status);
|
||||
Assert.Equal(4, source.CollisionStats.Loaded);
|
||||
Assert.True(source.MappedVirtualBytes > 1L << 30);
|
||||
}
|
||||
|
||||
private static string? ResolveDatDir()
|
||||
{
|
||||
string? configured =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
|
||||
if (!string.IsNullOrWhiteSpace(configured)
|
||||
&& Directory.Exists(configured))
|
||||
{
|
||||
return configured;
|
||||
}
|
||||
|
||||
string fallback = Path.Combine(
|
||||
Environment.GetFolderPath(
|
||||
Environment.SpecialFolder.UserProfile),
|
||||
"Documents",
|
||||
"Asheron's Call");
|
||||
return Directory.Exists(fallback) ? fallback : null;
|
||||
}
|
||||
}
|
||||
|
|
@ -35,6 +35,10 @@ public class PakKeyTests {
|
|||
[InlineData(PakAssetType.GfxObjMesh, 1)]
|
||||
[InlineData(PakAssetType.SetupMesh, 2)]
|
||||
[InlineData(PakAssetType.EnvCellMesh, 3)]
|
||||
[InlineData(PakAssetType.GfxObjCollision, 4)]
|
||||
[InlineData(PakAssetType.SetupCollision, 5)]
|
||||
[InlineData(PakAssetType.CellStructureCollision, 6)]
|
||||
[InlineData(PakAssetType.EnvCellTopology, 7)]
|
||||
public void AssetType_NumericValuesAreStable(PakAssetType type, byte expected) {
|
||||
// These values are a wire format — pin them so a future refactor can't
|
||||
// silently renumber the enum and corrupt existing paks.
|
||||
|
|
|
|||
|
|
@ -43,6 +43,24 @@ public sealed class FlatCollisionInstalledDatTests
|
|||
FlatCollisionAssetBuilder.FlattenCell(source);
|
||||
FlatCellCollisionAsset second =
|
||||
FlatCollisionAssetBuilder.FlattenCell(source);
|
||||
EnvCell datCell = Assert.IsType<EnvCell>(
|
||||
dats.Get<EnvCell>(cellId));
|
||||
var environment =
|
||||
Assert.IsType<DatReaderWriter.DBObjs.Environment>(
|
||||
dats.Get<DatReaderWriter.DBObjs.Environment>(
|
||||
0x0D00_0000u | datCell.EnvironmentId));
|
||||
Assert.True(
|
||||
environment.Cells.TryGetValue(
|
||||
datCell.CellStructure,
|
||||
out CellStruct? cellStruct));
|
||||
Assert.NotNull(cellStruct);
|
||||
FlatCellStructureCollisionAsset directStructure =
|
||||
FlatCollisionAssetBuilder.FlattenCellStructure(cellStruct!);
|
||||
FlatEnvCellTopology directTopology =
|
||||
FlatCollisionAssetBuilder.FlattenEnvCellTopology(
|
||||
cellId,
|
||||
datCell,
|
||||
directStructure.PortalPolygons);
|
||||
|
||||
AssertPhysicsSourceMatches(source.BSP?.Root, source.Resolved, first.Structure.PhysicsBsp);
|
||||
AssertContainmentSourceMatches(
|
||||
|
|
@ -60,6 +78,22 @@ public sealed class FlatCollisionInstalledDatTests
|
|||
Assert.Equal(
|
||||
first.Topology.VisibleCellIds.AsEnumerable(),
|
||||
second.Topology.VisibleCellIds.AsEnumerable());
|
||||
FlatCollisionAssetBuilderTests.AssertFlatPhysicsEqual(
|
||||
first.Structure.PhysicsBsp,
|
||||
directStructure.PhysicsBsp);
|
||||
Assert.Equal(
|
||||
first.Structure.ContainmentBsp.Nodes.AsEnumerable(),
|
||||
directStructure.ContainmentBsp.Nodes.AsEnumerable());
|
||||
AssertPolygonTableEqual(
|
||||
first.Structure.PortalPolygons,
|
||||
directStructure.PortalPolygons);
|
||||
Assert.Equal(
|
||||
first.Topology.Portals.AsEnumerable(),
|
||||
directTopology.Portals.AsEnumerable());
|
||||
Assert.Equal(
|
||||
first.Topology.VisibleCellIds.AsEnumerable(),
|
||||
directTopology.VisibleCellIds.AsEnumerable());
|
||||
Assert.Equal(first.Topology.SeenOutside, directTopology.SeenOutside);
|
||||
|
||||
_output.WriteLine(
|
||||
$"cell 0x{cellId:X8}: physicsNodes={first.Structure.PhysicsBsp.Nodes.Length}, " +
|
||||
|
|
@ -97,6 +131,8 @@ public sealed class FlatCollisionInstalledDatTests
|
|||
FlatCollisionAssetBuilder.FlattenGfxObj(
|
||||
source,
|
||||
cache.GetVisualBounds(gfxObjId));
|
||||
FlatGfxObjCollisionAsset direct =
|
||||
FlatCollisionAssetBuilder.FlattenGfxObj(gfxObj);
|
||||
|
||||
AssertPhysicsSourceMatches(
|
||||
source.BSP.Root,
|
||||
|
|
@ -107,6 +143,15 @@ public sealed class FlatCollisionInstalledDatTests
|
|||
AssertVisualBoundsBits(
|
||||
Assert.IsType<GfxObjVisualBounds>(cache.GetVisualBounds(gfxObjId)),
|
||||
flat.VisualBounds!.Value);
|
||||
FlatCollisionAssetBuilderTests.AssertFlatPhysicsEqual(
|
||||
flat.PhysicsBsp,
|
||||
direct.PhysicsBsp);
|
||||
AssertFlatNullableSphereBits(
|
||||
flat.BoundingSphere,
|
||||
direct.BoundingSphere);
|
||||
AssertFlatNullableVisualBoundsBits(
|
||||
flat.VisualBounds,
|
||||
direct.VisualBounds);
|
||||
|
||||
_output.WriteLine(
|
||||
$"gfx 0x{gfxObjId:X8}: nodes={flat.PhysicsBsp.Nodes.Length}, " +
|
||||
|
|
@ -127,7 +172,10 @@ public sealed class FlatCollisionInstalledDatTests
|
|||
|
||||
FlatSetupCollision flat =
|
||||
FlatCollisionAssetBuilder.FlattenSetup(source);
|
||||
FlatSetupCollision direct =
|
||||
FlatCollisionAssetBuilder.FlattenSetup(setup);
|
||||
AssertSetupSourceMatches(source, flat);
|
||||
AssertSetupFlatEqual(flat, direct);
|
||||
|
||||
_output.WriteLine(
|
||||
$"setup 0x{setupId:X8}: cylinders={flat.Cylinders.Length}, " +
|
||||
|
|
@ -327,4 +375,114 @@ public sealed class FlatCollisionInstalledDatTests
|
|||
source.HalfExtents,
|
||||
flat.HalfExtents);
|
||||
}
|
||||
|
||||
private static void AssertFlatNullableSphereBits(
|
||||
FlatCollisionSphere? expected,
|
||||
FlatCollisionSphere? actual)
|
||||
{
|
||||
Assert.Equal(expected.HasValue, actual.HasValue);
|
||||
if (!expected.HasValue || !actual.HasValue)
|
||||
return;
|
||||
FlatCollisionAssetBuilderTests.AssertVectorBits(
|
||||
expected.Value.Origin,
|
||||
actual.Value.Origin);
|
||||
FlatCollisionAssetBuilderTests.AssertFloatBits(
|
||||
expected.Value.Radius,
|
||||
actual.Value.Radius);
|
||||
}
|
||||
|
||||
private static void AssertFlatNullableVisualBoundsBits(
|
||||
FlatGfxObjVisualBounds? expected,
|
||||
FlatGfxObjVisualBounds? actual)
|
||||
{
|
||||
Assert.Equal(expected.HasValue, actual.HasValue);
|
||||
if (!expected.HasValue || !actual.HasValue)
|
||||
return;
|
||||
FlatCollisionAssetBuilderTests.AssertVectorBits(
|
||||
expected.Value.Min,
|
||||
actual.Value.Min);
|
||||
FlatCollisionAssetBuilderTests.AssertVectorBits(
|
||||
expected.Value.Max,
|
||||
actual.Value.Max);
|
||||
FlatCollisionAssetBuilderTests.AssertVectorBits(
|
||||
expected.Value.Center,
|
||||
actual.Value.Center);
|
||||
FlatCollisionAssetBuilderTests.AssertFloatBits(
|
||||
expected.Value.Radius,
|
||||
actual.Value.Radius);
|
||||
FlatCollisionAssetBuilderTests.AssertVectorBits(
|
||||
expected.Value.HalfExtents,
|
||||
actual.Value.HalfExtents);
|
||||
}
|
||||
|
||||
private static void AssertSetupFlatEqual(
|
||||
FlatSetupCollision expected,
|
||||
FlatSetupCollision actual)
|
||||
{
|
||||
Assert.Equal(expected.Cylinders.Length, actual.Cylinders.Length);
|
||||
Assert.Equal(expected.Spheres.Length, actual.Spheres.Length);
|
||||
for (int i = 0; i < expected.Cylinders.Length; i++)
|
||||
{
|
||||
FlatCollisionAssetBuilderTests.AssertVectorBits(
|
||||
expected.Cylinders[i].Origin,
|
||||
actual.Cylinders[i].Origin);
|
||||
FlatCollisionAssetBuilderTests.AssertFloatBits(
|
||||
expected.Cylinders[i].Radius,
|
||||
actual.Cylinders[i].Radius);
|
||||
FlatCollisionAssetBuilderTests.AssertFloatBits(
|
||||
expected.Cylinders[i].Height,
|
||||
actual.Cylinders[i].Height);
|
||||
}
|
||||
for (int i = 0; i < expected.Spheres.Length; i++)
|
||||
{
|
||||
FlatCollisionAssetBuilderTests.AssertVectorBits(
|
||||
expected.Spheres[i].Origin,
|
||||
actual.Spheres[i].Origin);
|
||||
FlatCollisionAssetBuilderTests.AssertFloatBits(
|
||||
expected.Spheres[i].Radius,
|
||||
actual.Spheres[i].Radius);
|
||||
}
|
||||
FlatCollisionAssetBuilderTests.AssertFloatBits(
|
||||
expected.Height,
|
||||
actual.Height);
|
||||
FlatCollisionAssetBuilderTests.AssertFloatBits(
|
||||
expected.Radius,
|
||||
actual.Radius);
|
||||
FlatCollisionAssetBuilderTests.AssertFloatBits(
|
||||
expected.StepUpHeight,
|
||||
actual.StepUpHeight);
|
||||
FlatCollisionAssetBuilderTests.AssertFloatBits(
|
||||
expected.StepDownHeight,
|
||||
actual.StepDownHeight);
|
||||
}
|
||||
|
||||
private static void AssertPolygonTableEqual(
|
||||
FlatPolygonTable expected,
|
||||
FlatPolygonTable actual)
|
||||
{
|
||||
Assert.Equal(expected.Polygons.Length, actual.Polygons.Length);
|
||||
Assert.Equal(expected.Vertices.Length, actual.Vertices.Length);
|
||||
for (int i = 0; i < expected.Polygons.Length; i++)
|
||||
{
|
||||
Assert.Equal(expected.Polygons[i].Id, actual.Polygons[i].Id);
|
||||
Assert.Equal(
|
||||
expected.Polygons[i].SidesType,
|
||||
actual.Polygons[i].SidesType);
|
||||
Assert.Equal(
|
||||
expected.Polygons[i].NumPoints,
|
||||
actual.Polygons[i].NumPoints);
|
||||
Assert.Equal(
|
||||
expected.Polygons[i].VertexRange,
|
||||
actual.Polygons[i].VertexRange);
|
||||
FlatCollisionAssetBuilderTests.AssertPlaneBits(
|
||||
expected.Polygons[i].Plane,
|
||||
actual.Polygons[i].Plane);
|
||||
}
|
||||
for (int i = 0; i < expected.Vertices.Length; i++)
|
||||
{
|
||||
FlatCollisionAssetBuilderTests.AssertVectorBits(
|
||||
expected.Vertices[i],
|
||||
actual.Vertices[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue