Optimize prepared asset package v2
This commit is contained in:
parent
d123c4b67c
commit
0dd966f3a0
28 changed files with 1277 additions and 106 deletions
|
|
@ -153,7 +153,7 @@ public sealed class BakeDeterminismTests : IDisposable {
|
|||
Assert.Equal(1, reportA.UniqueCellStructureCollisions);
|
||||
Assert.Equal(7, reportA.CellStructureCollisionAliases);
|
||||
Assert.Equal(8, reportA.EnvCellTopologyKeys);
|
||||
Assert.Equal(10, reportA.PhysicalBlobs);
|
||||
Assert.Equal(10 + reportA.TexturePayloadKeys, reportA.PhysicalBlobs);
|
||||
Assert.Equal(reportA.TotalKeys, reportB.TotalKeys);
|
||||
Assert.True(File.ReadAllBytes(pathA).AsSpan().SequenceEqual(File.ReadAllBytes(pathB)));
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ public sealed class InstalledPreparedCollisionCatalogTests
|
|||
if (datDir is null)
|
||||
Assert.Fail("Lane=PreparedPackage requires installed retail DATs and a validated acdream.pak; see docs/release-gate.md.");
|
||||
|
||||
string packagePath = Path.Combine(datDir, "acdream.pak");
|
||||
string packagePath = ResolvePackagePath(datDir);
|
||||
if (!File.Exists(packagePath))
|
||||
Assert.Fail("Lane=PreparedPackage requires installed retail DATs and a validated acdream.pak; see docs/release-gate.md.");
|
||||
|
||||
|
|
@ -36,7 +36,17 @@ public sealed class InstalledPreparedCollisionCatalogTests
|
|||
PreparedAssetReadStatus.Loaded,
|
||||
source.ReadEnvCellTopology(0xA9B4_013Fu).Status);
|
||||
Assert.Equal(4, source.CollisionStats.Loaded);
|
||||
Assert.True(source.MappedVirtualBytes > 1L << 30);
|
||||
Assert.Equal(new FileInfo(packagePath).Length, source.MappedVirtualBytes);
|
||||
Assert.InRange(source.MappedVirtualBytes, 1L, 5L * 1024 * 1024 * 1024);
|
||||
}
|
||||
|
||||
private static string ResolvePackagePath(string datDir)
|
||||
{
|
||||
string? configured =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PAK_PATH");
|
||||
return !string.IsNullOrWhiteSpace(configured)
|
||||
? Path.GetFullPath(configured)
|
||||
: Path.Combine(datDir, "acdream.pak");
|
||||
}
|
||||
|
||||
private static string? ResolveDatDir()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
|
|||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using AcDream.Content;
|
||||
using Chorizite.Core.Render.Enums;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Enums;
|
||||
|
|
@ -112,6 +113,100 @@ public sealed class MeshExtractorSolidFaceExtractionTests
|
|||
Assert.False(batch.Key.IsSolid);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(PixelFormat.PFID_DXT1, TextureFormat.DXT1, 8)]
|
||||
[InlineData(PixelFormat.PFID_DXT3, TextureFormat.DXT3, 16)]
|
||||
[InlineData(PixelFormat.PFID_DXT5, TextureFormat.DXT5, 16)]
|
||||
public void PrepareMeshData_UneditedDxtSurface_PreservesNativeBlocks(
|
||||
PixelFormat sourceFormat,
|
||||
TextureFormat expectedFormat,
|
||||
int sourceBytes)
|
||||
{
|
||||
var dats = new FakeMeshExtractorDats();
|
||||
byte[] blocks = Enumerable.Range(0, sourceBytes).Select(i => (byte)i).ToArray();
|
||||
RegisterTexturedQuad(dats, SurfaceType.Base1Image, sourceFormat, blocks);
|
||||
|
||||
var extractor = new MeshExtractor(dats, NullLogger.Instance, sideStagedSink: null);
|
||||
ObjectMeshData mesh = Assert.IsType<ObjectMeshData>(
|
||||
extractor.PrepareMeshData(GfxObjId, isSetup: false));
|
||||
|
||||
KeyValuePair<(int Width, int Height, TextureFormat Format), List<TextureBatchData>> group =
|
||||
Assert.Single(mesh.TextureBatches);
|
||||
Assert.Equal(expectedFormat, group.Key.Format);
|
||||
TextureBatchData batch = Assert.Single(group.Value);
|
||||
Assert.Same(blocks, batch.TextureData);
|
||||
Assert.Null(batch.UploadPixelFormat);
|
||||
Assert.Null(batch.UploadPixelType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrepareMeshData_DxtClipMap_DecodesForSurfaceLocalAlphaEdit()
|
||||
{
|
||||
var dats = new FakeMeshExtractorDats();
|
||||
RegisterTexturedQuad(
|
||||
dats,
|
||||
SurfaceType.Base1Image | SurfaceType.Base1ClipMap,
|
||||
PixelFormat.PFID_DXT1,
|
||||
new byte[8]);
|
||||
|
||||
var extractor = new MeshExtractor(dats, NullLogger.Instance, sideStagedSink: null);
|
||||
ObjectMeshData mesh = Assert.IsType<ObjectMeshData>(
|
||||
extractor.PrepareMeshData(GfxObjId, isSetup: false));
|
||||
|
||||
KeyValuePair<(int Width, int Height, TextureFormat Format), List<TextureBatchData>> group =
|
||||
Assert.Single(mesh.TextureBatches);
|
||||
Assert.Equal(TextureFormat.RGBA8, group.Key.Format);
|
||||
Assert.Equal(4 * 4 * 4, Assert.Single(group.Value).TextureData.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrepareMeshData_TranslucentDxt_DecodesForAlphaScale()
|
||||
{
|
||||
var dats = new FakeMeshExtractorDats();
|
||||
RegisterTexturedQuad(
|
||||
dats,
|
||||
SurfaceType.Base1Image,
|
||||
PixelFormat.PFID_DXT1,
|
||||
new byte[8],
|
||||
translucency: 0.25f);
|
||||
|
||||
var extractor = new MeshExtractor(dats, NullLogger.Instance, sideStagedSink: null);
|
||||
ObjectMeshData mesh = Assert.IsType<ObjectMeshData>(
|
||||
extractor.PrepareMeshData(GfxObjId, isSetup: false));
|
||||
|
||||
KeyValuePair<(int Width, int Height, TextureFormat Format), List<TextureBatchData>> group =
|
||||
Assert.Single(mesh.TextureBatches);
|
||||
Assert.Equal(TextureFormat.RGBA8, group.Key.Format);
|
||||
Assert.Equal(4 * 4 * 4, Assert.Single(group.Value).TextureData.Length);
|
||||
}
|
||||
|
||||
private static void RegisterTexturedQuad(
|
||||
FakeMeshExtractorDats dats,
|
||||
SurfaceType surfaceType,
|
||||
PixelFormat pixelFormat,
|
||||
byte[] sourceData,
|
||||
float translucency = 0.0f)
|
||||
{
|
||||
dats.RegisterRootGfxObj(GfxObjId, BuildQuadGfxObj(TexturedSurfaceId, noPos: false));
|
||||
dats.Register(TexturedSurfaceId, new Surface
|
||||
{
|
||||
Type = surfaceType,
|
||||
OrigTextureId = SurfaceTextureId,
|
||||
Translucency = translucency,
|
||||
});
|
||||
dats.Register(SurfaceTextureId, new SurfaceTexture
|
||||
{
|
||||
Textures = new List<QualifiedDataId<RenderSurface>> { RenderSurfaceId },
|
||||
});
|
||||
dats.Register(RenderSurfaceId, new RenderSurface
|
||||
{
|
||||
Width = 4,
|
||||
Height = 4,
|
||||
Format = pixelFormat,
|
||||
SourceData = sourceData,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>One quad (4 verts), single polygon, PosSurface referencing <paramref name="surfaceId"/>.</summary>
|
||||
private static GfxObj BuildQuadGfxObj(uint surfaceId, bool noPos)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ public sealed class PakEquivalenceTests {
|
|||
var pakPath = Path.Combine(Path.GetTempPath(), $"acdream-equivtest-{System.Guid.NewGuid():N}.pak");
|
||||
try {
|
||||
var header = new PakHeader {
|
||||
FormatVersion = 1,
|
||||
FormatVersion = PakFormat.CurrentFormatVersion,
|
||||
PortalIteration = (uint)dats.Portal.Iteration!.CurrentIteration,
|
||||
CellIteration = (uint)dats.Cell.Iteration!.CurrentIteration,
|
||||
HighResIteration = (uint)dats.HighRes.Iteration!.CurrentIteration,
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ public class PakRoundTripTests : IDisposable {
|
|||
|
||||
private static PakHeader WritePak(string path, (PakAssetType Type, uint FileId, ObjectMeshData Data)[] blobs) {
|
||||
var header = new PakHeader {
|
||||
FormatVersion = 1,
|
||||
FormatVersion = PakFormat.CurrentFormatVersion,
|
||||
PortalIteration = 10,
|
||||
CellIteration = 20,
|
||||
HighResIteration = 30,
|
||||
|
|
@ -231,7 +231,8 @@ public class PakRoundTripTests : IDisposable {
|
|||
|
||||
[Theory]
|
||||
[InlineData(0u)]
|
||||
[InlineData(2u)]
|
||||
[InlineData(1u)]
|
||||
[InlineData(3u)]
|
||||
public void Reader_RejectsWrongFormatVersion(uint wrongVersion) {
|
||||
var path = NewTempPakPath();
|
||||
WritePak(path, MakeBlobSet(2));
|
||||
|
|
@ -339,7 +340,7 @@ public class PakRoundTripTests : IDisposable {
|
|||
|
||||
// Recompute CRC over the tampered blob region.
|
||||
fs.Position = (long)entry.Offset;
|
||||
var blobBytes = new byte[entry.Length];
|
||||
var blobBytes = new byte[entry.StoredLength];
|
||||
fs.ReadExactly(blobBytes);
|
||||
uint newCrc = Crc32.Compute(blobBytes);
|
||||
|
||||
|
|
|
|||
292
tests/AcDream.Content.Tests/PakV2Tests.cs
Normal file
292
tests/AcDream.Content.Tests/PakV2Tests.cs
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
using System.Buffers.Binary;
|
||||
using System.Security.Cryptography;
|
||||
using AcDream.Content.Pak;
|
||||
using Chorizite.Core.Render.Enums;
|
||||
using DatReaderWriter.Enums;
|
||||
using RetailCullMode = DatReaderWriter.Enums.CullMode;
|
||||
|
||||
namespace AcDream.Content.Tests;
|
||||
|
||||
public sealed class PakV2Tests : IDisposable
|
||||
{
|
||||
private readonly List<string> _paths = [];
|
||||
|
||||
[Fact]
|
||||
public void BlobCodec_UsesRawFallbackAndCompressedRoundTripsExactly()
|
||||
{
|
||||
byte[] small = [1, 2, 3, 4];
|
||||
PakBlobCodec.Encoded raw = PakBlobCodec.Encode(small);
|
||||
Assert.False(raw.Compressed);
|
||||
Assert.Equal(small, raw.Bytes);
|
||||
|
||||
byte[] repetitive = new byte[64 * 1024];
|
||||
for (int i = 0; i < repetitive.Length; i++)
|
||||
repetitive[i] = (byte)(i & 7);
|
||||
|
||||
PakBlobCodec.Encoded compressed = PakBlobCodec.Encode(repetitive);
|
||||
Assert.True(compressed.Compressed);
|
||||
Assert.True(compressed.Bytes.Length < repetitive.Length / 4);
|
||||
Assert.Equal(repetitive, PakBlobCodec.Decode(compressed.Bytes));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(new byte[] { })]
|
||||
[InlineData(new byte[] { 1, 2, 3 })]
|
||||
[InlineData(new byte[] { 4, 0, 0, 0, 255 })]
|
||||
public void BlobCodec_RejectsMalformedCompressedPayload(byte[] stored) =>
|
||||
Assert.Throws<InvalidDataException>(() => PakBlobCodec.Decode(stored));
|
||||
|
||||
[Fact]
|
||||
public void BlobCodec_RejectsAllocationBombBeforeAllocating()
|
||||
{
|
||||
byte[] stored = new byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||
stored,
|
||||
PakBlobCodec.MaximumDecodedBytes + 1u);
|
||||
|
||||
Assert.Throws<InvalidDataException>(() => PakBlobCodec.Decode(stored));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Writer_DeduplicatesTextureBytesGloballyAndReaderSharesArray()
|
||||
{
|
||||
string path = NewPath();
|
||||
byte[] texture = new byte[16 * 1024];
|
||||
for (int i = 0; i < texture.Length; i++)
|
||||
texture[i] = (byte)(i & 15);
|
||||
|
||||
ObjectMeshData first = TexturedMesh(1, 0x0800_0001u, texture);
|
||||
ObjectMeshData second = TexturedMesh(2, 0x0800_0002u, texture.ToArray());
|
||||
using (var writer = NewWriter(path))
|
||||
{
|
||||
writer.AddBlob(PakKey.Compose(PakAssetType.GfxObjMesh, 1), first);
|
||||
writer.AddBlob(PakKey.Compose(PakAssetType.GfxObjMesh, 2), second);
|
||||
Assert.Equal(1, writer.TextureBlobCount);
|
||||
Assert.Equal(3, writer.EntryCount);
|
||||
writer.Finish();
|
||||
}
|
||||
|
||||
using var reader = new PakReader(path);
|
||||
Assert.Equal(1, reader.CountEntries(PakAssetType.TexturePayload));
|
||||
Assert.True(reader.TryReadObjectMeshData(
|
||||
PakKey.Compose(PakAssetType.GfxObjMesh, 1),
|
||||
out ObjectMeshData? firstRead));
|
||||
Assert.True(reader.TryReadObjectMeshData(
|
||||
PakKey.Compose(PakAssetType.GfxObjMesh, 2),
|
||||
out ObjectMeshData? secondRead));
|
||||
ObjectMeshDataEquality.AssertEqual(first, firstRead);
|
||||
ObjectMeshDataEquality.AssertEqual(second, secondRead);
|
||||
Assert.Same(
|
||||
firstRead!.TextureBatches.Single().Value.Single().TextureData,
|
||||
secondRead!.TextureBatches.Single().Value.Single().TextureData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Writer_IsByteDeterministicWithExternalTexturesAndCompression()
|
||||
{
|
||||
string firstPath = NewPath();
|
||||
string secondPath = NewPath();
|
||||
byte[] texture = Enumerable.Repeat((byte)0x5A, 32 * 1024).ToArray();
|
||||
|
||||
WriteDeterministic(firstPath, texture);
|
||||
WriteDeterministic(secondPath, texture);
|
||||
|
||||
Assert.Equal(File.ReadAllBytes(firstPath), File.ReadAllBytes(secondPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CorruptTextureDemotesOnlyReferencingMesh()
|
||||
{
|
||||
string path = NewPath();
|
||||
byte[] badTexture = Enumerable.Repeat((byte)0x11, 16 * 1024).ToArray();
|
||||
byte[] goodTexture = Enumerable.Repeat((byte)0x22, 16 * 1024).ToArray();
|
||||
ulong badMeshKey = PakKey.Compose(PakAssetType.GfxObjMesh, 1);
|
||||
ulong goodMeshKey = PakKey.Compose(PakAssetType.GfxObjMesh, 2);
|
||||
ulong badTextureKey = TexturePayloadKey(badTexture);
|
||||
using (var writer = NewWriter(path))
|
||||
{
|
||||
writer.AddBlob(badMeshKey, TexturedMesh(1, 1, badTexture));
|
||||
writer.AddBlob(goodMeshKey, TexturedMesh(2, 2, goodTexture));
|
||||
writer.Finish();
|
||||
}
|
||||
|
||||
PakTocEntry textureEntry;
|
||||
using (var reader = new PakReader(path))
|
||||
textureEntry = reader.GetTocEntryForTest(badTextureKey);
|
||||
|
||||
using (var stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite))
|
||||
{
|
||||
stream.Position = (long)textureEntry.Offset + textureEntry.StoredLength / 2;
|
||||
int value = stream.ReadByte();
|
||||
stream.Position--;
|
||||
stream.WriteByte((byte)(value ^ 0xFF));
|
||||
}
|
||||
|
||||
using var corruptReader = new PakReader(path);
|
||||
Assert.Equal(
|
||||
PakObjectReadStatus.Corrupt,
|
||||
corruptReader.ReadObjectMeshData(badMeshKey, out _));
|
||||
Assert.Equal(PakEntryState.Corrupt, corruptReader.ProbeEntry(badTextureKey));
|
||||
Assert.Equal(
|
||||
PakObjectReadStatus.Loaded,
|
||||
corruptReader.ReadObjectMeshData(goodMeshKey, out ObjectMeshData? good));
|
||||
Assert.Equal(goodTexture, good!.TextureBatches.Single().Value.Single().TextureData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reader_DemotesInvalidCompressedStreamBehindValidCrc()
|
||||
{
|
||||
string path = NewPath();
|
||||
ulong key = PakKey.Compose(PakAssetType.GfxObjCollision, 7);
|
||||
using (var writer = NewWriter(path))
|
||||
{
|
||||
writer.AddBlob(key, new byte[32 * 1024]);
|
||||
writer.Finish();
|
||||
}
|
||||
|
||||
PakTocEntry entry;
|
||||
long tocEntryPosition;
|
||||
using (var reader = new PakReader(path))
|
||||
{
|
||||
entry = reader.GetTocEntryForTest(key);
|
||||
Assert.True(entry.IsCompressed);
|
||||
tocEntryPosition = FindTocEntryPosition(path, key);
|
||||
}
|
||||
|
||||
byte[] invalid = new byte[checked((int)entry.StoredLength)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(invalid, 32 * 1024);
|
||||
invalid.AsSpan(sizeof(uint)).Fill(0xFF);
|
||||
using (var stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite))
|
||||
{
|
||||
stream.Position = (long)entry.Offset;
|
||||
stream.Write(invalid);
|
||||
stream.Position = tocEntryPosition + 20;
|
||||
Span<byte> crc = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(crc, Crc32.Compute(invalid));
|
||||
stream.Write(crc);
|
||||
}
|
||||
|
||||
using var corruptReader = new PakReader(path);
|
||||
Assert.Equal(
|
||||
PakObjectReadStatus.Corrupt,
|
||||
corruptReader.ReadBlobBytes(key, out byte[]? bytes));
|
||||
Assert.Null(bytes);
|
||||
Assert.Equal(PakEntryState.Corrupt, corruptReader.ProbeEntry(key));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TextureCache_IsReferenceSharingAndStrictlyBounded()
|
||||
{
|
||||
var cache = new PakTexturePayloadCache(maximumBytes: 6, maximumEntries: 2);
|
||||
byte[] first = [1, 2, 3, 4];
|
||||
byte[] second = [5, 6, 7, 8];
|
||||
|
||||
Assert.Same(first, cache.AddOrGet(1, first));
|
||||
Assert.Same(first, cache.AddOrGet(1, first.ToArray()));
|
||||
Assert.Same(second, cache.AddOrGet(2, second));
|
||||
Assert.False(cache.TryGet(1, out _));
|
||||
Assert.True(cache.TryGet(2, out byte[] cachedSecond));
|
||||
Assert.Same(second, cachedSecond);
|
||||
Assert.Equal(1, cache.Count);
|
||||
Assert.Equal(4, cache.Bytes);
|
||||
|
||||
byte[] tooLarge = new byte[7];
|
||||
Assert.Same(tooLarge, cache.AddOrGet(3, tooLarge));
|
||||
Assert.False(cache.TryGet(3, out _));
|
||||
Assert.Equal(4, cache.Bytes);
|
||||
}
|
||||
|
||||
private static ObjectMeshData TexturedMesh(
|
||||
uint objectId,
|
||||
uint surfaceId,
|
||||
byte[] texture)
|
||||
{
|
||||
var mesh = new ObjectMeshData { ObjectId = objectId };
|
||||
mesh.TextureBatches[(64, 64, TextureFormat.RGBA8)] =
|
||||
[
|
||||
new TextureBatchData
|
||||
{
|
||||
Key = new TextureKey
|
||||
{
|
||||
SurfaceId = surfaceId,
|
||||
PaletteId = 1,
|
||||
Stippling = StipplingType.Positive,
|
||||
},
|
||||
TextureData = texture,
|
||||
Indices = [0, 1, 2],
|
||||
CullMode = RetailCullMode.Clockwise,
|
||||
},
|
||||
];
|
||||
return mesh;
|
||||
}
|
||||
|
||||
private static PakWriter NewWriter(string path) =>
|
||||
new(path, new PakHeader
|
||||
{
|
||||
PortalIteration = 1,
|
||||
CellIteration = 2,
|
||||
HighResIteration = 3,
|
||||
LanguageIteration = 4,
|
||||
});
|
||||
|
||||
private static void WriteDeterministic(string path, byte[] texture)
|
||||
{
|
||||
using var writer = NewWriter(path);
|
||||
writer.AddBlob(
|
||||
PakKey.Compose(PakAssetType.GfxObjMesh, 2),
|
||||
TexturedMesh(2, 12, texture.ToArray()));
|
||||
writer.AddBlob(
|
||||
PakKey.Compose(PakAssetType.GfxObjMesh, 1),
|
||||
TexturedMesh(1, 11, texture.ToArray()));
|
||||
writer.Finish();
|
||||
}
|
||||
|
||||
private static ulong TexturePayloadKey(byte[] bytes)
|
||||
{
|
||||
byte[] digest = SHA256.HashData(bytes);
|
||||
ulong payloadId =
|
||||
((ulong)digest[0] << 48)
|
||||
| ((ulong)digest[1] << 40)
|
||||
| ((ulong)digest[2] << 32)
|
||||
| ((ulong)digest[3] << 24)
|
||||
| ((ulong)digest[4] << 16)
|
||||
| ((ulong)digest[5] << 8)
|
||||
| digest[6];
|
||||
return PakKey.ComposeOpaque(PakAssetType.TexturePayload, payloadId);
|
||||
}
|
||||
|
||||
private string NewPath()
|
||||
{
|
||||
string path = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
$"acdream-pak-v2-{Guid.NewGuid():N}.pak");
|
||||
_paths.Add(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
private static long FindTocEntryPosition(string path, ulong key)
|
||||
{
|
||||
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
|
||||
PakHeader header = PakHeader.ReadFrom(stream);
|
||||
var bytes = new byte[PakTocEntry.Size];
|
||||
for (uint i = 0; i < header.TocCount; i++)
|
||||
{
|
||||
long position = checked((long)header.TocOffset + i * PakTocEntry.Size);
|
||||
stream.Position = position;
|
||||
stream.ReadExactly(bytes);
|
||||
if (PakTocEntry.ReadFrom(bytes).Key == key)
|
||||
return position;
|
||||
}
|
||||
|
||||
throw new KeyNotFoundException($"pak key 0x{key:X16} not found");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (string path in _paths)
|
||||
{
|
||||
try { File.Delete(path); }
|
||||
catch (IOException) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
using AcDream.Launcher.Core.Installation;
|
||||
|
||||
namespace AcDream.Launcher.Core.Tests.Installation;
|
||||
|
||||
public sealed class ContentMigrationCatalogTests
|
||||
{
|
||||
[Fact]
|
||||
public void RecipeFiveToSixRequiresOneExplicitFullRebuild()
|
||||
{
|
||||
ContentMigrationPlan plan = ContentMigrationCatalog.Resolve(5, 6);
|
||||
|
||||
Assert.Equal(ContentWorkKind.FullRebuild, plan.Kind);
|
||||
Assert.Equal(5u, plan.FromRecipeVersion);
|
||||
Assert.Equal(6u, plan.TargetRecipeVersion);
|
||||
Assert.Contains("pak v2", plan.Reason, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Empty(plan.EffectiveDatIds);
|
||||
Assert.Empty(plan.EffectiveLandblocks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnyOlderRecipeToSixCollapsesToOneFullRebuild()
|
||||
{
|
||||
ContentMigrationPlan plan = ContentMigrationCatalog.Resolve(1, 6);
|
||||
|
||||
Assert.Equal(ContentWorkKind.FullRebuild, plan.Kind);
|
||||
Assert.Equal(6u, plan.TargetRecipeVersion);
|
||||
Assert.Contains("pak v2", plan.Reason, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
|
@ -133,7 +133,7 @@ public sealed class LauncherContentStateStoreTests : IDisposable
|
|||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
byte[] bytes = new byte[64];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(0, 4), 0x4B504341u);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(4, 4), 1);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(4, 4), 2);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(8, 4), 100);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(12, 4), 200);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(16, 4), 300);
|
||||
|
|
|
|||
|
|
@ -147,6 +147,33 @@ public sealed class LauncherInstallerTests : IDisposable
|
|||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InsufficientFreeSpaceStopsBeforeBakeAndReportsExactRequirement()
|
||||
{
|
||||
bool childStarted = false;
|
||||
var runner = new FakeBakeProcessRunner((_, _, _) =>
|
||||
{
|
||||
childStarted = true;
|
||||
return Task.FromResult(new BakeProcessResult(0, string.Empty));
|
||||
});
|
||||
var installer = new LauncherInstaller(
|
||||
_paths,
|
||||
_bakeExecutable,
|
||||
processRunner: runner,
|
||||
availableFreeSpace: _ =>
|
||||
LauncherInstaller.FullRebuildRequiredFreeBytes - 1);
|
||||
|
||||
LauncherInstallException error = await Assert.ThrowsAsync<LauncherInstallException>(
|
||||
() => installer.InstallAsync(_dats, threads: 2));
|
||||
|
||||
Assert.False(childStarted);
|
||||
Assert.Contains("2.0 GiB", error.Message, StringComparison.Ordinal);
|
||||
Assert.Contains("active package", error.Message, StringComparison.Ordinal);
|
||||
Assert.False(File.Exists(
|
||||
LauncherInstaller.GetFullRebuildCandidatePath(
|
||||
Path.Combine(_paths.DataDirectory, "pak", "acdream.pak"))));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FullContentMigrationPersistsClientGateAcrossLauncherRestart()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -58,8 +58,8 @@ public sealed class LauncherOverlayInstallerTests : IDisposable
|
|||
request.OutputPath,
|
||||
LauncherInstallRecordStore.CurrentBakeToolVersion);
|
||||
long bytes = new FileInfo(request.OutputPath).Length;
|
||||
output($"{{\"v\":1,\"e\":\"started\",\"bakeToolVersion\":5}}\n");
|
||||
output($"{{\"v\":1,\"e\":\"completed\",\"bakeToolVersion\":5,"
|
||||
output($"{{\"v\":1,\"e\":\"started\",\"bakeToolVersion\":{LauncherInstallRecordStore.CurrentBakeToolVersion}}}\n");
|
||||
output($"{{\"v\":1,\"e\":\"completed\",\"bakeToolVersion\":{LauncherInstallRecordStore.CurrentBakeToolVersion},"
|
||||
+ $"\"outputBytes\":{bytes},\"failures\":0}}\n");
|
||||
await Task.Yield();
|
||||
return new BakeProcessResult(0, string.Empty);
|
||||
|
|
@ -70,8 +70,8 @@ public sealed class LauncherOverlayInstallerTests : IDisposable
|
|||
recordStore: recordStore,
|
||||
processRunner: runner);
|
||||
var migration = new ContentMigrationPlan(
|
||||
4,
|
||||
5,
|
||||
LauncherInstallRecordStore.CurrentBakeToolVersion - 1,
|
||||
LauncherInstallRecordStore.CurrentBakeToolVersion,
|
||||
ContentWorkKind.Overlay,
|
||||
"bounded fixture update",
|
||||
[0x01001234u, 0x02005678u],
|
||||
|
|
@ -93,7 +93,9 @@ public sealed class LauncherOverlayInstallerTests : IDisposable
|
|||
observed.Arguments.SkipWhile(value => value != "--landblocks").Take(2));
|
||||
Assert.NotNull(result.Record.PreparedAssetOverlayPath);
|
||||
Assert.True(File.Exists(result.Record.PreparedAssetOverlayPath));
|
||||
Assert.Equal(5u, result.Record.ResolvedBakeToolVersion);
|
||||
Assert.Equal(
|
||||
LauncherInstallRecordStore.CurrentBakeToolVersion,
|
||||
result.Record.ResolvedBakeToolVersion);
|
||||
Assert.True(result.Record.RequiresClientCompatibilityConfirmation);
|
||||
Assert.False(File.Exists(
|
||||
new LauncherContentStateStore(_paths).OverlayCandidatePath));
|
||||
|
|
@ -145,7 +147,9 @@ public sealed class LauncherOverlayInstallerTests : IDisposable
|
|||
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var runner = new FakeRunner(async (request, _, cancellationToken) =>
|
||||
{
|
||||
LauncherContentStateStoreTests.WritePakHeader(request.OutputPath, 5);
|
||||
LauncherContentStateStoreTests.WritePakHeader(
|
||||
request.OutputPath,
|
||||
LauncherInstallRecordStore.CurrentBakeToolVersion);
|
||||
entered.SetResult();
|
||||
await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
|
||||
return new BakeProcessResult(0, string.Empty);
|
||||
|
|
@ -160,8 +164,8 @@ public sealed class LauncherOverlayInstallerTests : IDisposable
|
|||
_dats,
|
||||
2,
|
||||
new ContentMigrationPlan(
|
||||
4,
|
||||
5,
|
||||
LauncherInstallRecordStore.CurrentBakeToolVersion - 1,
|
||||
LauncherInstallRecordStore.CurrentBakeToolVersion,
|
||||
ContentWorkKind.Overlay,
|
||||
"bounded fixture update",
|
||||
[0x01001234u]),
|
||||
|
|
@ -189,11 +193,11 @@ public sealed class LauncherOverlayInstallerTests : IDisposable
|
|||
{
|
||||
LauncherContentStateStoreTests.WritePakHeader(
|
||||
recordStore.PreparedAssetPath,
|
||||
recipe: 4);
|
||||
recipe: LauncherInstallRecordStore.CurrentBakeToolVersion - 1);
|
||||
LauncherInstallRecord baseRecord =
|
||||
await LauncherContentStateStoreTests.RecordAsync(
|
||||
recordStore.PreparedAssetPath,
|
||||
recipe: 4,
|
||||
recipe: LauncherInstallRecordStore.CurrentBakeToolVersion - 1,
|
||||
datDirectory: Path.GetFullPath(_dats));
|
||||
Directory.CreateDirectory(_paths.DataDirectory);
|
||||
await File.WriteAllTextAsync(
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@ public sealed class LauncherWindowViewModelTests
|
|||
Assert.Equal("World data update required", viewModel.InstallationBannerTitle);
|
||||
Assert.Contains("complete replacement pak", viewModel.FirstRunWizardShell.Body);
|
||||
Assert.Contains("existing package stays", viewModel.FirstRunWizardShell.Body);
|
||||
Assert.Contains("2 GiB", viewModel.FirstRunWizardShell.Body);
|
||||
Assert.Equal("Rebuild world data", viewModel.FirstRunWizardShell.StartActionText);
|
||||
Assert.Null(installer.InstallRequest);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue