acdream/tests/AcDream.Content.Tests/PakFormatTests.cs
Erik acf172469e feat(content): S1 exact CellStruct surface-index construction, recipe 8
Campaign OVERHAUL S1 chunk A. Retail's D3DPolyRender::ConstructMesh
@0x0059DFA0 is ported as one pure Core descriptor plus the Content
extraction that consumes it:

- side candidates come only from sides_type (0/1/2); NoPos/NoNeg mean
  UV-array absence only and never suppress a side (CPolygon::UnPack
  @0x00538650);
- ST_DOUBLE's second copy is reversed with a negative normal; ST_BOTH's
  negative side has a negative normal and forward fan order (reverse is on
  the copy ordinal, not the side ordinal);
- an absent UV-index array is UV index 0 (ConstructMesh @0x0059E691
  xor ebx,ebx, arbitrated on the PDB-paired binary); copyVert @0x0059C080
  zeroes coordinates only for a negative or out-of-range index or a vertex
  without UVs, never by clamping to slot 0;
- the subset owner is the source surface-array index, emitted in ascending
  slot order with retail's per-slot mask (2 > 8 > 4 precedence, positive
  surface OR on signed stippling > 0);
- built-EnvCell admission is (Surface.Type & (BASE1_IMAGE|BASE1_CLIPMAP))
  != 0 after surface resolution (DrawEnvCell @0x0059F170 -> DrawMesh
  @0x0059D4A0 arg4=1); untextured slots are constructed but not emitted;
- cell batches carry SourceSurfaceIndex, RetailSurfaceMask, RawSurfaceType,
  IsCellShell, and fixed clockwise raster cull (RenderMeshSubset
  @0x0059CA10); authored sides_type is no longer stored as GPU cull.

Prepared-mesh serializer gains the four fields; bake recipe 7 -> 8 with a
FullRebuild migration; pak format stays 2 (pinned). Ordinary GfxObj
extraction is unchanged. AP-234's register row and CellMesh unification
land in chunk B.

Core: 32 descriptor tests. Content: 170/170. Bake: 18/18. Launcher.Core:
365/365 (Lane!=Linux). Solution Release build 0 warnings / 0 errors.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 18:40:33 +02:00

157 lines
6.2 KiB
C#

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);
}
/// <summary>
/// OH2/S1 contract §8.3 point 5: the recipe 7 -&gt; 8 bump (exact
/// CellStruct surface-index subset construction) is a serialized
/// PAYLOAD/recipe change, not a container framing change — this pin
/// makes that decision explicit and test-visible rather than implicit.
/// </summary>
[Fact]
public void FormatVersion_StaysAtTwo_AcrossTheOH2RecipeBump() {
Assert.Equal(2u, PakFormat.CurrentFormatVersion);
Assert.Equal(8u, PakFormat.CurrentBakeToolVersion);
}
[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]));
}
}