feat(pipeline): MP1b - pak key + header/TOC primitives
TDD: PakKeyTests + PakFormatTests written first (confirmed a compile failure against the not-yet-existing AcDream.Content.Pak namespace), then PakKey (64-bit type:u8|fileId:u32|reserved:u24 compose/decompose) and PakFormat (64-byte PakHeader, 24-byte PakTocEntry) implemented to the normative layout in the MP1b plan. 21 tests green, including a key- ordering test proving ascending numeric key order equals ascending (type, fileId) tuple order (the TOC binary-search precondition) and an explicit byte-offset test for both structs.
This commit is contained in:
parent
f29255a45c
commit
8248abe9d4
4 changed files with 371 additions and 8 deletions
124
src/AcDream.Content/Pak/PakFormat.cs
Normal file
124
src/AcDream.Content/Pak/PakFormat.cs
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
using System;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace AcDream.Content.Pak;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fixed 64-byte pak file header. Layout (all integers little-endian):
|
||||||
|
/// <code>
|
||||||
|
/// offset size field
|
||||||
|
/// 0 4 magic 'ACPK' (0x4B504341)
|
||||||
|
/// 4 4 formatVersion = 1
|
||||||
|
/// 8 4 portalIteration (DatCollection.Portal.Iteration)
|
||||||
|
/// 12 4 cellIteration
|
||||||
|
/// 16 4 highResIteration
|
||||||
|
/// 20 4 languageIteration
|
||||||
|
/// 24 8 tocOffset (u64)
|
||||||
|
/// 32 4 tocCount (u32)
|
||||||
|
/// 36 4 bakeToolVersion = 1
|
||||||
|
/// 40 24 reserved (zero)
|
||||||
|
/// </code>
|
||||||
|
/// Spec: docs/superpowers/plans/2026-07-05-mp1b-pak-and-bake.md "Format v1 (normative)".
|
||||||
|
/// </summary>
|
||||||
|
public struct PakHeader {
|
||||||
|
public const int Size = 64;
|
||||||
|
public const uint MagicValue = 0x4B504341u; // 'ACPK' little-endian
|
||||||
|
|
||||||
|
/// <summary>Always <see cref="MagicValue"/> after <see cref="ReadFrom(ReadOnlySpan{byte})"/>; not settable by callers building a header to write.</summary>
|
||||||
|
public uint Magic { get; private set; } = MagicValue;
|
||||||
|
|
||||||
|
public uint FormatVersion;
|
||||||
|
public uint PortalIteration;
|
||||||
|
public uint CellIteration;
|
||||||
|
public uint HighResIteration;
|
||||||
|
public uint LanguageIteration;
|
||||||
|
public ulong TocOffset;
|
||||||
|
public uint TocCount;
|
||||||
|
public uint BakeToolVersion;
|
||||||
|
|
||||||
|
public PakHeader() { }
|
||||||
|
|
||||||
|
public void WriteTo(Span<byte> dest) {
|
||||||
|
if (dest.Length < Size) throw new ArgumentException($"destination must be at least {Size} bytes", nameof(dest));
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(dest[0..4], MagicValue);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(dest[4..8], FormatVersion);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(dest[8..12], PortalIteration);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(dest[12..16], CellIteration);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(dest[16..20], HighResIteration);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(dest[20..24], LanguageIteration);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(dest[24..32], TocOffset);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(dest[32..36], TocCount);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(dest[36..40], BakeToolVersion);
|
||||||
|
dest[40..64].Clear(); // reserved, zero
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteTo(Stream stream) {
|
||||||
|
Span<byte> buf = stackalloc byte[Size];
|
||||||
|
WriteTo(buf);
|
||||||
|
stream.Write(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PakHeader ReadFrom(ReadOnlySpan<byte> src) {
|
||||||
|
if (src.Length < Size) throw new ArgumentException($"source must be at least {Size} bytes", nameof(src));
|
||||||
|
var magic = BinaryPrimitives.ReadUInt32LittleEndian(src[0..4]);
|
||||||
|
if (magic != MagicValue) {
|
||||||
|
throw new InvalidDataException($"pak header magic mismatch: expected 0x{MagicValue:X8}, got 0x{magic:X8}");
|
||||||
|
}
|
||||||
|
return new PakHeader {
|
||||||
|
FormatVersion = BinaryPrimitives.ReadUInt32LittleEndian(src[4..8]),
|
||||||
|
PortalIteration = BinaryPrimitives.ReadUInt32LittleEndian(src[8..12]),
|
||||||
|
CellIteration = BinaryPrimitives.ReadUInt32LittleEndian(src[12..16]),
|
||||||
|
HighResIteration = BinaryPrimitives.ReadUInt32LittleEndian(src[16..20]),
|
||||||
|
LanguageIteration = BinaryPrimitives.ReadUInt32LittleEndian(src[20..24]),
|
||||||
|
TocOffset = BinaryPrimitives.ReadUInt64LittleEndian(src[24..32]),
|
||||||
|
TocCount = BinaryPrimitives.ReadUInt32LittleEndian(src[32..36]),
|
||||||
|
BakeToolVersion = BinaryPrimitives.ReadUInt32LittleEndian(src[36..40]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PakHeader ReadFrom(Stream stream) {
|
||||||
|
Span<byte> buf = stackalloc byte[Size];
|
||||||
|
stream.ReadExactly(buf);
|
||||||
|
return ReadFrom((ReadOnlySpan<byte>)buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One 24-byte TOC entry: <c>key u64, offset u64, length u32, crc32 u32</c>.
|
||||||
|
/// Entries in a pak's TOC are sorted ascending by <see cref="Key"/> to allow
|
||||||
|
/// binary-search lookup. <see cref="Crc32"/> is a corruption tripwire computed
|
||||||
|
/// over the blob bytes; the reader verifies lazily on first access.
|
||||||
|
/// </summary>
|
||||||
|
public struct PakTocEntry {
|
||||||
|
public const int Size = 24;
|
||||||
|
|
||||||
|
public ulong Key;
|
||||||
|
public ulong Offset;
|
||||||
|
public uint Length;
|
||||||
|
public uint Crc32;
|
||||||
|
|
||||||
|
public void WriteTo(Span<byte> dest) {
|
||||||
|
if (dest.Length < Size) throw new ArgumentException($"destination must be at least {Size} bytes", nameof(dest));
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(dest[0..8], Key);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(dest[8..16], Offset);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(dest[16..20], Length);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(dest[20..24], Crc32);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteTo(Stream stream) {
|
||||||
|
Span<byte> buf = stackalloc byte[Size];
|
||||||
|
WriteTo(buf);
|
||||||
|
stream.Write(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PakTocEntry ReadFrom(ReadOnlySpan<byte> src) {
|
||||||
|
if (src.Length < Size) throw new ArgumentException($"source must be at least {Size} bytes", nameof(src));
|
||||||
|
return new PakTocEntry {
|
||||||
|
Key = BinaryPrimitives.ReadUInt64LittleEndian(src[0..8]),
|
||||||
|
Offset = BinaryPrimitives.ReadUInt64LittleEndian(src[8..16]),
|
||||||
|
Length = BinaryPrimitives.ReadUInt32LittleEndian(src[16..20]),
|
||||||
|
Crc32 = BinaryPrimitives.ReadUInt32LittleEndian(src[20..24]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/AcDream.Content/Pak/PakKey.cs
Normal file
33
src/AcDream.Content/Pak/PakKey.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
namespace AcDream.Content.Pak;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MP1b pak asset-type discriminant — the top byte of a <see cref="PakKey"/>.
|
||||||
|
/// Numeric values are a WIRE FORMAT (persisted in every pak's TOC): never
|
||||||
|
/// renumber existing members, only append.
|
||||||
|
/// Spec: docs/superpowers/specs/2026-07-05-modern-pipeline-design.md §6.2;
|
||||||
|
/// plan: docs/superpowers/plans/2026-07-05-mp1b-pak-and-bake.md "Format v1".
|
||||||
|
/// </summary>
|
||||||
|
public enum PakAssetType : byte {
|
||||||
|
GfxObjMesh = 1,
|
||||||
|
SetupMesh = 2,
|
||||||
|
EnvCellMesh = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Composes/decomposes the 64-bit pak asset key: <c>type:u8 | fileId:u32 | reserved:u24</c>.
|
||||||
|
/// Layout: <c>((ulong)type << 56) | ((ulong)fileId << 24)</c> — the low 24 bits are
|
||||||
|
/// reserved (variant/zero in v1). Ascending numeric key order equals ascending
|
||||||
|
/// (type, fileId) tuple order, which is what makes the pak TOC's binary search
|
||||||
|
/// over raw u64 keys valid.
|
||||||
|
/// </summary>
|
||||||
|
public static class PakKey {
|
||||||
|
public static ulong Compose(PakAssetType type, uint fileId) {
|
||||||
|
return ((ulong)type << 56) | ((ulong)fileId << 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (PakAssetType Type, uint FileId) Decompose(ulong key) {
|
||||||
|
var type = (PakAssetType)(byte)(key >> 56);
|
||||||
|
var fileId = (uint)((key >> 24) & 0xFFFFFFFFu);
|
||||||
|
return (type, fileId);
|
||||||
|
}
|
||||||
|
}
|
||||||
145
tests/AcDream.Content.Tests/PakFormatTests.cs
Normal file
145
tests/AcDream.Content.Tests/PakFormatTests.cs
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
using System.IO;
|
||||||
|
using AcDream.Content.Pak;
|
||||||
|
|
||||||
|
namespace AcDream.Content.Tests;
|
||||||
|
|
||||||
|
public class PakFormatTests {
|
||||||
|
[Fact]
|
||||||
|
public void Header_Size_Is64Bytes() {
|
||||||
|
Assert.Equal(64, PakHeader.Size);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TocEntry_Size_Is24Bytes() {
|
||||||
|
Assert.Equal(24, PakTocEntry.Size);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Header_Magic_IsAcpkLittleEndian() {
|
||||||
|
// 'A'=0x41 'C'=0x43 'P'=0x50 'K'=0x4B — little-endian dword reads
|
||||||
|
// back as 0x4B504341 per the normative spec.
|
||||||
|
Assert.Equal(0x4B504341u, PakHeader.MagicValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Header_WriteThenRead_RoundTripsEveryField() {
|
||||||
|
var header = new PakHeader {
|
||||||
|
FormatVersion = 1,
|
||||||
|
PortalIteration = 111,
|
||||||
|
CellIteration = 222,
|
||||||
|
HighResIteration = 333,
|
||||||
|
LanguageIteration = 444,
|
||||||
|
TocOffset = 0x1_0000_0002UL,
|
||||||
|
TocCount = 777,
|
||||||
|
BakeToolVersion = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
header.WriteTo(ms);
|
||||||
|
Assert.Equal(PakHeader.Size, ms.Length);
|
||||||
|
|
||||||
|
ms.Position = 0;
|
||||||
|
var readBack = PakHeader.ReadFrom(ms);
|
||||||
|
|
||||||
|
Assert.Equal(PakHeader.MagicValue, readBack.Magic);
|
||||||
|
Assert.Equal(header.FormatVersion, readBack.FormatVersion);
|
||||||
|
Assert.Equal(header.PortalIteration, readBack.PortalIteration);
|
||||||
|
Assert.Equal(header.CellIteration, readBack.CellIteration);
|
||||||
|
Assert.Equal(header.HighResIteration, readBack.HighResIteration);
|
||||||
|
Assert.Equal(header.LanguageIteration, readBack.LanguageIteration);
|
||||||
|
Assert.Equal(header.TocOffset, readBack.TocOffset);
|
||||||
|
Assert.Equal(header.TocCount, readBack.TocCount);
|
||||||
|
Assert.Equal(header.BakeToolVersion, readBack.BakeToolVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Header_ReadFrom_Span_RoundTrips() {
|
||||||
|
var header = new PakHeader {
|
||||||
|
FormatVersion = 1,
|
||||||
|
PortalIteration = 5,
|
||||||
|
CellIteration = 6,
|
||||||
|
HighResIteration = 7,
|
||||||
|
LanguageIteration = 8,
|
||||||
|
TocOffset = 999,
|
||||||
|
TocCount = 3,
|
||||||
|
BakeToolVersion = 1,
|
||||||
|
};
|
||||||
|
Span<byte> buf = stackalloc byte[PakHeader.Size];
|
||||||
|
header.WriteTo(buf);
|
||||||
|
var readBack = PakHeader.ReadFrom((ReadOnlySpan<byte>)buf);
|
||||||
|
Assert.Equal(header.CellIteration, readBack.CellIteration);
|
||||||
|
Assert.Equal(header.TocOffset, readBack.TocOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Header_ReservedBytes_AreZero() {
|
||||||
|
var header = new PakHeader { FormatVersion = 1, BakeToolVersion = 1 };
|
||||||
|
Span<byte> buf = stackalloc byte[PakHeader.Size];
|
||||||
|
header.WriteTo(buf);
|
||||||
|
// offset 40, length 24 per the normative layout
|
||||||
|
for (int i = 40; i < 64; i++) {
|
||||||
|
Assert.Equal(0, buf[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Header_FieldOffsets_MatchNormativeLayout() {
|
||||||
|
var header = new PakHeader {
|
||||||
|
FormatVersion = 0x11111111,
|
||||||
|
PortalIteration = 0x22222222,
|
||||||
|
CellIteration = 0x33333333,
|
||||||
|
HighResIteration = 0x44444444,
|
||||||
|
LanguageIteration = 0x55555555,
|
||||||
|
TocOffset = 0x6666666677777777UL,
|
||||||
|
TocCount = 0x88888888,
|
||||||
|
BakeToolVersion = 0x99999999,
|
||||||
|
};
|
||||||
|
Span<byte> buf = stackalloc byte[PakHeader.Size];
|
||||||
|
header.WriteTo(buf);
|
||||||
|
|
||||||
|
Assert.Equal(PakHeader.MagicValue, System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(buf[0..4]));
|
||||||
|
Assert.Equal(0x11111111u, System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(buf[4..8]));
|
||||||
|
Assert.Equal(0x22222222u, System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(buf[8..12]));
|
||||||
|
Assert.Equal(0x33333333u, System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(buf[12..16]));
|
||||||
|
Assert.Equal(0x44444444u, System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(buf[16..20]));
|
||||||
|
Assert.Equal(0x55555555u, System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(buf[20..24]));
|
||||||
|
Assert.Equal(0x6666666677777777UL, System.Buffers.Binary.BinaryPrimitives.ReadUInt64LittleEndian(buf[24..32]));
|
||||||
|
Assert.Equal(0x88888888u, System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(buf[32..36]));
|
||||||
|
Assert.Equal(0x99999999u, System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(buf[36..40]));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TocEntry_WriteThenRead_RoundTrips() {
|
||||||
|
var entry = new PakTocEntry {
|
||||||
|
Key = PakKey.Compose(PakAssetType.GfxObjMesh, 0x010002B4u),
|
||||||
|
Offset = 0x4000,
|
||||||
|
Length = 12345,
|
||||||
|
Crc32 = 0xDEADBEEF,
|
||||||
|
};
|
||||||
|
Span<byte> buf = stackalloc byte[PakTocEntry.Size];
|
||||||
|
entry.WriteTo(buf);
|
||||||
|
var readBack = PakTocEntry.ReadFrom((ReadOnlySpan<byte>)buf);
|
||||||
|
|
||||||
|
Assert.Equal(entry.Key, readBack.Key);
|
||||||
|
Assert.Equal(entry.Offset, readBack.Offset);
|
||||||
|
Assert.Equal(entry.Length, readBack.Length);
|
||||||
|
Assert.Equal(entry.Crc32, readBack.Crc32);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TocEntry_FieldOffsets_MatchNormativeLayout() {
|
||||||
|
var entry = new PakTocEntry {
|
||||||
|
Key = 0x1111111122222222UL,
|
||||||
|
Offset = 0x3333333344444444UL,
|
||||||
|
Length = 0x55555555,
|
||||||
|
Crc32 = 0x66666666,
|
||||||
|
};
|
||||||
|
Span<byte> buf = stackalloc byte[PakTocEntry.Size];
|
||||||
|
entry.WriteTo(buf);
|
||||||
|
|
||||||
|
Assert.Equal(0x1111111122222222UL, System.Buffers.Binary.BinaryPrimitives.ReadUInt64LittleEndian(buf[0..8]));
|
||||||
|
Assert.Equal(0x3333333344444444UL, System.Buffers.Binary.BinaryPrimitives.ReadUInt64LittleEndian(buf[8..16]));
|
||||||
|
Assert.Equal(0x55555555u, System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(buf[16..20]));
|
||||||
|
Assert.Equal(0x66666666u, System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(buf[20..24]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,75 @@
|
||||||
|
using AcDream.Content.Pak;
|
||||||
|
|
||||||
namespace AcDream.Content.Tests;
|
namespace AcDream.Content.Tests;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Task 1 scaffold smoke test — becomes the real PakKey test suite in Task 2.
|
|
||||||
/// </summary>
|
|
||||||
public class PakKeyTests {
|
public class PakKeyTests {
|
||||||
|
[Theory]
|
||||||
|
[InlineData(PakAssetType.GfxObjMesh, 0u)]
|
||||||
|
[InlineData(PakAssetType.GfxObjMesh, 0x01000001u)]
|
||||||
|
[InlineData(PakAssetType.SetupMesh, 0x020019FFu)]
|
||||||
|
[InlineData(PakAssetType.EnvCellMesh, 0xA9B40100u)]
|
||||||
|
[InlineData(PakAssetType.GfxObjMesh, 0xFFFFFFFFu)] // max fileId
|
||||||
|
[InlineData(PakAssetType.EnvCellMesh, 0xFFFFFFFFu)]
|
||||||
|
public void ComposeDecompose_RoundTrips(PakAssetType type, uint fileId) {
|
||||||
|
ulong key = PakKey.Compose(type, fileId);
|
||||||
|
var (decodedType, decodedFileId) = PakKey.Decompose(key);
|
||||||
|
Assert.Equal(type, decodedType);
|
||||||
|
Assert.Equal(fileId, decodedFileId);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Smoke_ProjectReferencesContentAssembly() {
|
public void Compose_LowFourBytesReserved() {
|
||||||
// Any type from AcDream.Content resolves — proves the ProjectReference
|
// Low 24 bits are reserved (zero in v1) — fileId << 24 must not spill
|
||||||
// and test-project wiring are correct before Task 2 adds real coverage.
|
// into them, and the reserved region must actually read back as zero.
|
||||||
var key = new TextureKey();
|
ulong key = PakKey.Compose(PakAssetType.GfxObjMesh, 0xFFFFFFFFu);
|
||||||
Assert.Equal(0u, key.SurfaceId);
|
Assert.Equal(0u, (uint)(key & 0xFFFFFFu));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Compose_TypeOccupiesTopByte() {
|
||||||
|
ulong key = PakKey.Compose(PakAssetType.SetupMesh, 0u);
|
||||||
|
Assert.Equal((byte)PakAssetType.SetupMesh, (byte)(key >> 56));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(PakAssetType.GfxObjMesh, 1)]
|
||||||
|
[InlineData(PakAssetType.SetupMesh, 2)]
|
||||||
|
[InlineData(PakAssetType.EnvCellMesh, 3)]
|
||||||
|
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.
|
||||||
|
Assert.Equal(expected, (byte)type);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void KeyOrdering_MatchesTypeThenFileIdOrdering() {
|
||||||
|
// Ascending key order must equal ascending (type, fileId) tuple order —
|
||||||
|
// this is what makes the TOC's binary search over raw u64 keys valid.
|
||||||
|
var pairs = new (PakAssetType Type, uint FileId)[] {
|
||||||
|
(PakAssetType.GfxObjMesh, 0u),
|
||||||
|
(PakAssetType.GfxObjMesh, 1u),
|
||||||
|
(PakAssetType.GfxObjMesh, 0xFFFFFFFFu),
|
||||||
|
(PakAssetType.SetupMesh, 0u),
|
||||||
|
(PakAssetType.SetupMesh, 0x020019FFu),
|
||||||
|
(PakAssetType.EnvCellMesh, 0u),
|
||||||
|
(PakAssetType.EnvCellMesh, 0xA9B40100u),
|
||||||
|
};
|
||||||
|
|
||||||
|
var keys = pairs.Select(p => PakKey.Compose(p.Type, p.FileId)).ToArray();
|
||||||
|
|
||||||
|
// keys[] as generated must already be strictly ascending since pairs[]
|
||||||
|
// is in ascending tuple order.
|
||||||
|
for (int i = 1; i < keys.Length; i++) {
|
||||||
|
Assert.True(keys[i] > keys[i - 1],
|
||||||
|
$"key[{i}]=0x{keys[i]:X16} should be > key[{i - 1}]=0x{keys[i - 1]:X16} " +
|
||||||
|
$"for tuple order ({pairs[i - 1]}) < ({pairs[i]})");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sorting the keys numerically must reproduce the same order as sorting
|
||||||
|
// the tuples lexicographically by (Type, FileId).
|
||||||
|
var numericSorted = keys.OrderBy(k => k).ToArray();
|
||||||
|
var tupleSorted = pairs.OrderBy(p => (byte)p.Type).ThenBy(p => p.FileId)
|
||||||
|
.Select(p => PakKey.Compose(p.Type, p.FileId)).ToArray();
|
||||||
|
Assert.Equal(tupleSorted, numericSorted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue