diff --git a/src/NwcIO/Internal/Parsing/StreamClassifier.cs b/src/NwcIO/Internal/Parsing/StreamClassifier.cs index 6d93291..41657d2 100644 --- a/src/NwcIO/Internal/Parsing/StreamClassifier.cs +++ b/src/NwcIO/Internal/Parsing/StreamClassifier.cs @@ -60,6 +60,34 @@ public static NwcStreamKind Classify(LcRawStream stream) return NwcStreamKind.Transforms; } + // Mesh record IDs are arbitrary. Recognize a vertex buffer before the GUID heuristic. + if (data.Length >= 24 && ReadInt32(data, 4) is 94 or 95 && ReadInt32(data, 12) == 60) + { + var p1 = ReadInt32(data, 16); + var p2 = ReadInt32(data, 20); + var vertexCount = p2 > 0 && p2 % 3 == 0 ? p2 / 3 : p1; + if (vertexCount >= 3 && 24L + 12L * vertexCount <= data.Length) + { + return NwcStreamKind.Geometry; + } + } + + // TOC: [int32 recCount][int32 chunkCount][chunkCount x int32 sizes][recCount x int32 lengths] + // Its size and chunk count can also satisfy the GUID-table heuristic. + if (data.Length >= 16) + { + var recCount = ReadInt32(data, 0); + var chunkCount = ReadInt32(data, 4); + if (recCount > 0 && recCount < 1_000_000 && chunkCount > 0 && chunkCount < 5000) + { + var expected = 8 + 4 * chunkCount + 4 * recCount; + if (data.Length == expected || data.Length == expected + 4) + { + return NwcStreamKind.Toc; + } + } + } + // GUID table: [a][count] followed by exactly count 16-byte GUIDs. if (data.Length >= 8 && (data.Length - 8) % 16 == 0) { @@ -87,21 +115,6 @@ public static NwcStreamKind Classify(LcRawStream stream) return NwcStreamKind.Geometry; } - // TOC: [int32 recCount][int32 chunkCount][chunkCount x int32 sizes][recCount x int32 lengths] - if (data.Length >= 16) - { - int recCount = ReadInt32(data, 0); - int chunkCount = ReadInt32(data, 4); - if (recCount > 0 && recCount < 1_000_000 && chunkCount > 0 && chunkCount < 5000) - { - int expected = 8 + 4 * chunkCount + 4 * recCount; - if (data.Length == expected || data.Length == expected + 4) - { - return NwcStreamKind.Toc; - } - } - } - // Saved viewpoints and cameras (starts with tag 517 + camera quaternion) if (data.Length >= 120 && IsSavedViewsStream(data)) { diff --git a/tests/NwcIO.Tests/StreamClassifierTests.cs b/tests/NwcIO.Tests/StreamClassifierTests.cs new file mode 100644 index 0000000..3ce771f --- /dev/null +++ b/tests/NwcIO.Tests/StreamClassifierTests.cs @@ -0,0 +1,40 @@ +using System.Buffers.Binary; +using NwcIO.Internal.Container; +using NwcIO.Internal.Parsing; +using Xunit; + +namespace NwcIO.Tests; + +public class StreamClassifierTests +{ + [Theory] + [InlineData(1)] + [InlineData(2)] + public void Classify_GuidShapedToc_ReturnsToc(int chunkCount) + { + var recordCount = 3 * chunkCount; + var data = new byte[8 + 4 * chunkCount + 4 * recordCount]; + BinaryPrimitives.WriteInt32LittleEndian(data, recordCount); + BinaryPrimitives.WriteInt32LittleEndian(data.AsSpan(4), chunkCount); + for (var i = 0; i < chunkCount; i++) + { + BinaryPrimitives.WriteInt32LittleEndian(data.AsSpan(8 + 4 * i), 180); + } + for (var i = 0; i < recordCount; i++) + { + BinaryPrimitives.WriteInt32LittleEndian(data.AsSpan(8 + 4 * chunkCount + 4 * i), 60); + } + + var stream = new LcRawStream + { + Index = chunkCount, + SectionIndex = 0, + FileOffset = 0, + CompressedLength = 0, + GapBefore = 0, + Data = data, + }; + + Assert.Equal(NwcStreamKind.Toc, StreamClassifier.Classify(stream)); + } +} diff --git a/tests/NwcIO.Tests/WriterTests.cs b/tests/NwcIO.Tests/WriterTests.cs index d32c555..5f466a7 100644 --- a/tests/NwcIO.Tests/WriterTests.cs +++ b/tests/NwcIO.Tests/WriterTests.cs @@ -1,4 +1,5 @@ using NwcIO.Internal.Container; +using NwcIO.Internal.Parsing; using NwcIO.Model; using Xunit; @@ -6,6 +7,44 @@ namespace NwcIO.Tests; public class WriterTests { + [Theory] + [InlineData(3, 3, false)] + [InlineData(3, 3, true)] + [InlineData(1, 124, false)] + [InlineData(1, 124, true)] + public void Document_GuidShapedGeometryAndToc_RoundtripPreservesGuids(int meshCount, int vertexCount, bool withGuids) + { + var meshes = Enumerable.Range(1, meshCount).Select(id => new NwcMesh + { + Id = id, + Vertices = Enumerable.Range(0, vertexCount).Select(v => new Vector3d(v, v % 2, id)).ToArray(), + Indices = [0, 1, 2], + }).ToArray(); + var guids = withGuids + ? new[] { Guid.Parse("00112233-4455-6677-8899-aabbccddeeff"), Guid.Parse("ffeeddcc-bbaa-9988-7766-554433221100") } + : Array.Empty(); + var doc = NwcDocument.Create(meshes: meshes, guids: guids); + var bytes = doc.ToByteArray(); + + using var ms = new MemoryStream(bytes); + var reopened = NwcDocument.Open(ms); + + Assert.Equal(guids, reopened.Guids); + Assert.Equal(meshCount, reopened.Meshes.Count); + for (var i = 0; i < meshCount; i++) + { + Assert.Equal(meshes[i].Vertices, reopened.Meshes[i].Vertices); + } + + var container = NwcContainerReader.Read(bytes); + StreamClassifier.ClassifyAll(container.Streams); + var toc = Assert.Single(container.Streams, s => s.Kind == NwcStreamKind.Toc); + var geometry = Assert.Single(container.Streams, s => s.Kind == NwcStreamKind.Geometry); + Assert.Equal(NwcStreamKind.Toc, StreamClassifier.Classify(toc)); + Assert.Equal(NwcStreamKind.Geometry, StreamClassifier.Classify(geometry)); + Assert.Equal(withGuids ? 1 : 0, container.Streams.Count(s => s.Kind == NwcStreamKind.Guids)); + } + [Fact] public void ContainerWriter_Roundtrip_RestoresExactStreamBuffers() {