acdream/src/AcDream.Bake/BakeRunner.cs
Erik 9cd42417a8 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.
2026-07-25 15:22:08 +02:00

1042 lines
43 KiB
C#

using System.Collections.Concurrent;
using System.Diagnostics;
using System.Numerics;
using AcDream.Content;
using AcDream.Content.Pak;
using AcDream.Core.Physics;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
using DatReaderWriter.Options;
using DatReaderWriter.Types;
using DatEnvironment = DatReaderWriter.DBObjs.Environment;
namespace AcDream.Bake;
/// <summary>Options for one bake run (parsed from CLI args by Program, or built directly by tests).</summary>
public sealed record BakeOptions
{
public required string DatDir { get; init; }
public required string OutPath { get; init; }
public HashSet<uint>? IdFilter { get; init; }
public HashSet<uint>? LandblockFilter { get; init; }
public int Threads { get; init; } = System.Environment.ProcessorCount;
public CancellationToken CancellationToken { get; init; }
}
/// <summary>Compact result used by the full-scale gate and deterministic tests.</summary>
public sealed record BakeReport
{
public required PakHeader Header { get; init; }
public required int GfxObjKeys { get; init; }
public required int SetupKeys { get; init; }
public required int EnvCellKeys { get; init; }
public required int UniqueEnvCellGeometries { get; init; }
public required int EnvCellAliases { get; init; }
public required int GfxObjCollisionKeys { get; init; }
public required int UniqueGfxObjCollisions { get; init; }
public required int GfxObjCollisionAliases { get; init; }
public required int SetupCollisionKeys { get; init; }
public required int UniqueSetupCollisions { get; init; }
public required int SetupCollisionAliases { get; init; }
public required int CellStructureCollisionKeys { get; init; }
public required int UniqueCellStructureCollisions { get; init; }
public required int CellStructureCollisionAliases { get; init; }
public required int EnvCellTopologyKeys { get; init; }
public required int SideStagedKeys { get; init; }
public int SideStagedDuplicateKeys { get; init; }
public required int PhysicalBlobs { get; init; }
public required int TotalKeys { get; init; }
public required int Failures { get; init; }
public required TimeSpan ExtractionAndWriteElapsed { get; init; }
public required TimeSpan Elapsed { get; init; }
public required long OutputBytes { get; init; }
public required long PeakWorkingSetBytes { get; init; }
public required long PeakPrivateBytes { get; init; }
public required IReadOnlyDictionary<PakAssetType, int> TypeCounts
{
get;
init;
}
public double EnvCellDedupRatio =>
UniqueEnvCellGeometries == 0 ? 0 : (double)EnvCellKeys / UniqueEnvCellGeometries;
}
/// <summary>
/// Offline content pipeline: enumerate source IDs, catalog EnvCell geometry by
/// the same content identity used at runtime, extract each unique payload once,
/// and stream deterministic blobs plus ordinary TOC aliases.
/// </summary>
public static class BakeRunner
{
/// <summary>
/// Items extracted in parallel per batch before sequential sorted write.
/// Bounds peak decoded output while keeping workers busy.
/// </summary>
private const int BatchSize = 16;
public static int Run(BakeOptions options)
{
RunDetailed(options);
return 0;
}
public static BakeReport RunDetailed(BakeOptions options)
{
ArgumentNullException.ThrowIfNull(options);
if (options.Threads <= 0)
throw new ArgumentOutOfRangeException(nameof(options), "thread count must be positive");
options.CancellationToken.ThrowIfCancellationRequested();
var totalStopwatch = Stopwatch.StartNew();
var report = BakeOutputTransaction.WriteValidateAndPublish(
options.OutPath,
temporaryPath => RunCore(
options with { OutPath = temporaryPath },
options.OutPath),
(temporaryPath, result) => BakeArtifactValidator.Validate(
temporaryPath,
result.Header,
result.TotalKeys,
result.TypeCounts),
options.CancellationToken);
totalStopwatch.Stop();
report = report with
{
Elapsed = totalStopwatch.Elapsed,
OutputBytes = new FileInfo(options.OutPath).Length,
PeakWorkingSetBytes = Process.GetCurrentProcess().PeakWorkingSet64,
PeakPrivateBytes = Process.GetCurrentProcess().PeakPagedMemorySize64,
};
PrintSummary(report, options.OutPath);
return report;
}
private static BakeReport RunCore(BakeOptions options, string publishedOutputPath)
{
var cancellationToken = options.CancellationToken;
Console.WriteLine("acdream-bake");
Console.WriteLine($"dat dir: {options.DatDir}");
Console.WriteLine($"out: {publishedOutputPath}");
Console.WriteLine($"threads: {options.Threads}");
Console.WriteLine();
var stopwatch = Stopwatch.StartNew();
using var dats = new DatCollection(options.DatDir, DatAccessType.Read);
using var datReaderWriter = new DatCollectionAdapter(dats);
var extractorLogger = new ConsoleErrorLogger(nameof(MeshExtractor));
var sideStaged = new ConcurrentQueue<ObjectMeshData>();
var extractor = new MeshExtractor(
datReaderWriter,
extractorLogger,
data => sideStaged.Enqueue(data));
var failures = new ConcurrentBag<(PakAssetType Type, uint FileId, string Reason)>();
// ---- enumeration and content-identity catalog ----------------------
var gfxObjIds = dats.GetAllIdsOfType<GfxObj>().OrderBy(id => id).ToList();
var setupIds = dats.GetAllIdsOfType<Setup>().OrderBy(id => id).ToList();
var envCellIds = EnumerateEnvCellIds(dats, options.LandblockFilter);
if (options.IdFilter is not null)
{
gfxObjIds = gfxObjIds.Where(options.IdFilter.Contains).ToList();
setupIds = setupIds.Where(options.IdFilter.Contains).ToList();
envCellIds = envCellIds.Where(options.IdFilter.Contains).ToList();
}
var envCatalogBuilder = new EnvCellBakeCatalogBuilder();
foreach (uint fileId in envCellIds)
{
cancellationToken.ThrowIfCancellationRequested();
if (!datReaderWriter.Cell.TryGet<EnvCell>(fileId, out var envCell) || envCell is null)
{
failures.Add((PakAssetType.EnvCellMesh, fileId, "EnvCell DAT record was not found"));
continue;
}
envCatalogBuilder.Add(
new EnvCellBakeSource(
fileId,
envCell.EnvironmentId,
envCell.CellStructure,
envCell.Surfaces));
}
var envCatalog = envCatalogBuilder.Build();
var ordinaryWork = new List<(ulong Key, PakAssetType Type, uint FileId)>(
gfxObjIds.Count + setupIds.Count);
ordinaryWork.AddRange(
gfxObjIds.Select(
id => (PakKey.Compose(PakAssetType.GfxObjMesh, id), PakAssetType.GfxObjMesh, id)));
ordinaryWork.AddRange(
setupIds.Select(
id => (PakKey.Compose(PakAssetType.SetupMesh, id), PakAssetType.SetupMesh, id)));
ordinaryWork.Sort((a, b) => a.Key.CompareTo(b.Key));
Console.WriteLine(
$"enumerated: {gfxObjIds.Count:N0} GfxObj, {setupIds.Count:N0} Setup, " +
$"{envCellIds.Count:N0} EnvCell");
Console.WriteLine(
$"EnvCell catalog: {envCatalog.CellCount:N0} valid cells -> " +
$"{envCatalog.UniqueGeometryCount:N0} unique geometries + " +
$"{envCatalog.AliasCount:N0} aliases " +
$"({(envCatalog.UniqueGeometryCount == 0 ? 0 : (double)envCatalog.CellCount / envCatalog.UniqueGeometryCount):F1}x)");
Console.WriteLine();
// ---- sorted-batch extraction and deterministic write ---------------
var header = new PakHeader
{
PortalIteration = (uint)dats.Portal.Iteration!.CurrentIteration,
CellIteration = (uint)dats.Cell.Iteration!.CurrentIteration,
HighResIteration = (uint)dats.HighRes.Iteration!.CurrentIteration,
LanguageIteration = (uint)dats.Local.Iteration!.CurrentIteration,
BakeToolVersion = PakFormat.CurrentBakeToolVersion,
};
var writtenKeys = new HashSet<ulong>();
var sideStagedByKey = new Dictionary<ulong, ObjectMeshData>();
long completed = 0;
int totalExtractionJobs = ordinaryWork.Count + envCatalog.UniqueGeometryCount;
int gfxObjWritten = 0;
int setupWritten = 0;
int envCellKeysWritten = 0;
int envCellGeometriesWritten = 0;
int sideStagedDuped = 0;
var lastProgressReport = Stopwatch.StartNew();
using (var writer = new PakWriter(options.OutPath, header))
{
for (int batchStart = 0; batchStart < ordinaryWork.Count; batchStart += BatchSize)
{
cancellationToken.ThrowIfCancellationRequested();
var batch = ordinaryWork.Skip(batchStart).Take(BatchSize).ToArray();
var batchResults =
new ConcurrentBag<(ulong Key, PakAssetType Type, ObjectMeshData Data)>();
Parallel.ForEach(
batch,
new ParallelOptions
{
MaxDegreeOfParallelism = options.Threads,
CancellationToken = cancellationToken,
},
item =>
{
var (key, type, fileId) = item;
try
{
bool isSetup = type == PakAssetType.SetupMesh;
var data = extractor.PrepareMeshData(fileId, isSetup, cancellationToken);
if (data is not null)
batchResults.Add((key, type, data));
else
failures.Add((type, fileId, "extractor returned null"));
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception exception)
{
failures.Add((type, fileId, exception.Message));
}
finally
{
Interlocked.Increment(ref completed);
}
});
foreach (var (key, type, data) in batchResults.OrderBy(result => result.Key))
{
writer.AddBlob(key, data);
writtenKeys.Add(key);
if (type == PakAssetType.GfxObjMesh)
gfxObjWritten++;
else
setupWritten++;
}
sideStagedDuped += DrainSideStaged(
sideStaged,
sideStagedByKey,
writtenKeys);
ReportProgressIfDue(
completed,
totalExtractionJobs,
failures.Count,
stopwatch.Elapsed,
lastProgressReport,
batchStart + BatchSize >= ordinaryWork.Count &&
envCatalog.UniqueGeometryCount == 0);
}
for (int batchStart = 0; batchStart < envCatalog.Groups.Count; batchStart += BatchSize)
{
cancellationToken.ThrowIfCancellationRequested();
var batch = envCatalog.Groups.Skip(batchStart).Take(BatchSize).ToArray();
var batchResults =
new ConcurrentBag<(EnvCellGeometryGroup Group, ObjectMeshData Data)>();
Parallel.ForEach(
batch,
new ParallelOptions
{
MaxDegreeOfParallelism = options.Threads,
CancellationToken = cancellationToken,
},
group =>
{
try
{
uint environmentDid = 0x0D00_0000u | group.EnvironmentId;
if (!datReaderWriter.Portal.TryGet<DatEnvironment>(
environmentDid,
out var environment) ||
environment is null ||
!environment.Cells.TryGetValue(
group.CellStructure,
out var cellStruct))
{
failures.Add((
PakAssetType.EnvCellMesh,
group.FileIds[0],
$"environment 0x{environmentDid:X8} cell structure " +
$"{group.CellStructure} was not found"));
return;
}
var data = extractor.PrepareCellStructMeshData(
group.GeometryId,
cellStruct,
group.Surfaces,
Matrix4x4.Identity,
cancellationToken);
if (data is not null)
batchResults.Add((group, data));
else
failures.Add((
PakAssetType.EnvCellMesh,
group.FileIds[0],
"unique cell-structure extraction returned null"));
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception exception)
{
failures.Add((
PakAssetType.EnvCellMesh,
group.FileIds[0],
exception.Message));
}
finally
{
Interlocked.Increment(ref completed);
}
});
foreach (var (group, data) in batchResults.OrderBy(result => result.Group.FileIds[0]))
{
ulong primaryKey =
PakKey.Compose(PakAssetType.EnvCellMesh, group.FileIds[0]);
writer.AddBlob(primaryKey, data);
writtenKeys.Add(primaryKey);
envCellKeysWritten++;
envCellGeometriesWritten++;
for (int i = 1; i < group.FileIds.Length; i++)
{
ulong aliasKey =
PakKey.Compose(PakAssetType.EnvCellMesh, group.FileIds[i]);
writer.AddAlias(aliasKey, primaryKey);
writtenKeys.Add(aliasKey);
envCellKeysWritten++;
}
}
sideStagedDuped += DrainSideStaged(
sideStaged,
sideStagedByKey,
writtenKeys);
ReportProgressIfDue(
completed,
totalExtractionJobs,
failures.Count,
stopwatch.Elapsed,
lastProgressReport,
batchStart + BatchSize >= envCatalog.Groups.Count);
}
int sideStagedWritten = 0;
foreach (var (key, data) in sideStagedByKey.OrderBy(pair => pair.Key))
{
cancellationToken.ThrowIfCancellationRequested();
if (writtenKeys.Contains(key))
{
sideStagedDuped++;
continue;
}
writer.AddBlob(key, data);
writtenKeys.Add(key);
sideStagedWritten++;
}
// ---- immutable flat collision payloads ------------------------
uint[] collisionGfxIds = gfxObjIds
.Concat(
sideStagedByKey.Keys.Select(
key => PakKey.Decompose(key).FileId))
.Distinct()
.Order()
.ToArray();
var collisionWork =
new List<(ulong Key, PakAssetType Type, uint FileId)>(
collisionGfxIds.Length + setupIds.Count);
collisionWork.AddRange(
collisionGfxIds.Select(
id => (
PakKey.Compose(PakAssetType.GfxObjCollision, id),
PakAssetType.GfxObjCollision,
id)));
collisionWork.AddRange(
setupIds.Select(
id => (
PakKey.Compose(PakAssetType.SetupCollision, id),
PakAssetType.SetupCollision,
id)));
collisionWork.Sort((left, right) => left.Key.CompareTo(right.Key));
var gfxCollisionAliases = new ExactPayloadAliasCatalog();
var setupCollisionAliases = new ExactPayloadAliasCatalog();
var cellStructureAliases = new ExactPayloadAliasCatalog();
int gfxCollisionWritten = 0;
int uniqueGfxCollisions = 0;
int gfxCollisionAliasCount = 0;
int setupCollisionWritten = 0;
int uniqueSetupCollisions = 0;
int setupCollisionAliasCount = 0;
int cellStructureWritten = 0;
int uniqueCellStructures = 0;
int cellStructureAliasCount = 0;
int envCellTopologyWritten = 0;
long collisionCompleted = 0;
int totalCollisionJobs =
collisionWork.Count
+ envCatalog.UniqueGeometryCount
+ envCatalog.CellCount;
var collisionStopwatch = Stopwatch.StartNew();
lastProgressReport.Restart();
Console.WriteLine();
Console.WriteLine(
$"collision catalog: {collisionGfxIds.Length:N0} GfxObj, " +
$"{setupIds.Count:N0} Setup, " +
$"{envCatalog.UniqueGeometryCount:N0} CellStruct source groups, " +
$"{envCatalog.CellCount:N0} EnvCell topology records");
for (int batchStart = 0;
batchStart < collisionWork.Count;
batchStart += BatchSize)
{
cancellationToken.ThrowIfCancellationRequested();
var batch = collisionWork
.Skip(batchStart)
.Take(BatchSize)
.ToArray();
var batchResults =
new ConcurrentBag<CollisionPayloadResult>();
Parallel.ForEach(
batch,
new ParallelOptions
{
MaxDegreeOfParallelism = options.Threads,
CancellationToken = cancellationToken,
},
item =>
{
var (key, type, fileId) = item;
try
{
byte[] payload;
if (type == PakAssetType.GfxObjCollision)
{
if (!datReaderWriter.Portal.TryGet<GfxObj>(
fileId,
out GfxObj? gfxObj)
|| gfxObj is null)
{
failures.Add((
type,
fileId,
"GfxObj DAT record was not found"));
return;
}
FlatGfxObjCollisionAsset asset =
FlatCollisionAssetBuilder.FlattenGfxObj(
gfxObj);
payload = FlatCollisionAssetSerializer.Serialize(
asset,
cancellationToken);
}
else
{
if (!datReaderWriter.Portal.TryGet<Setup>(
fileId,
out Setup? setup)
|| setup is null)
{
failures.Add((
type,
fileId,
"Setup DAT record was not found"));
return;
}
FlatSetupCollision asset =
FlatCollisionAssetBuilder.FlattenSetup(
setup);
payload = FlatCollisionAssetSerializer.Serialize(
asset,
cancellationToken);
}
batchResults.Add(
new CollisionPayloadResult(
key,
type,
fileId,
payload));
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception exception)
{
failures.Add((type, fileId, exception.Message));
}
finally
{
Interlocked.Increment(ref collisionCompleted);
}
});
foreach (CollisionPayloadResult result in
batchResults.OrderBy(result => result.Key))
{
ExactPayloadAliasCatalog aliases =
result.Type == PakAssetType.GfxObjCollision
? gfxCollisionAliases
: setupCollisionAliases;
bool alias = WriteExactPayload(
writer,
result.Key,
result.Payload,
aliases);
writtenKeys.Add(result.Key);
if (result.Type == PakAssetType.GfxObjCollision)
{
gfxCollisionWritten++;
if (alias)
gfxCollisionAliasCount++;
else
uniqueGfxCollisions++;
}
else
{
setupCollisionWritten++;
if (alias)
setupCollisionAliasCount++;
else
uniqueSetupCollisions++;
}
}
ReportProgressIfDue(
collisionCompleted,
totalCollisionJobs,
failures.Count,
collisionStopwatch.Elapsed,
lastProgressReport,
final: false);
}
for (int groupBatchStart = 0;
groupBatchStart < envCatalog.Groups.Count;
groupBatchStart += BatchSize)
{
cancellationToken.ThrowIfCancellationRequested();
EnvCellGeometryGroup[] groupBatch = envCatalog.Groups
.Skip(groupBatchStart)
.Take(BatchSize)
.ToArray();
var structureResults =
new ConcurrentBag<CellStructurePayloadResult>();
Parallel.ForEach(
groupBatch,
new ParallelOptions
{
MaxDegreeOfParallelism = options.Threads,
CancellationToken = cancellationToken,
},
group =>
{
try
{
uint environmentDid =
0x0D00_0000u | group.EnvironmentId;
if (!datReaderWriter.Portal.TryGet<DatEnvironment>(
environmentDid,
out DatEnvironment? environment)
|| environment is null
|| !environment.Cells.TryGetValue(
group.CellStructure,
out CellStruct? cellStruct)
|| cellStruct is null)
{
failures.Add((
PakAssetType.CellStructureCollision,
group.FileIds[0],
$"environment 0x{environmentDid:X8} cell " +
$"structure {group.CellStructure} was not found"));
Interlocked.Add(
ref collisionCompleted,
group.FileIds.Length);
return;
}
FlatCellStructureCollisionAsset structure =
FlatCollisionAssetBuilder.FlattenCellStructure(
cellStruct);
byte[] payload =
FlatCollisionAssetSerializer.Serialize(
structure,
cancellationToken);
structureResults.Add(
new CellStructurePayloadResult(
group,
structure,
payload));
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception exception)
{
failures.Add((
PakAssetType.CellStructureCollision,
group.FileIds[0],
exception.Message));
Interlocked.Add(
ref collisionCompleted,
group.FileIds.Length);
}
finally
{
Interlocked.Increment(ref collisionCompleted);
}
});
foreach (CellStructurePayloadResult result in
structureResults.OrderBy(
result => result.Group.FileIds[0]))
{
(int physical, int aliases) = WriteExactPayloadGroup(
writer,
PakAssetType.CellStructureCollision,
result.Group.FileIds,
result.Payload,
cellStructureAliases,
writtenKeys);
cellStructureWritten += result.Group.FileIds.Length;
uniqueCellStructures += physical;
cellStructureAliasCount += aliases;
for (int topologyBatchStart = 0;
topologyBatchStart < result.Group.FileIds.Length;
topologyBatchStart += BatchSize)
{
cancellationToken.ThrowIfCancellationRequested();
uint[] topologyBatch = result.Group.FileIds
.Skip(topologyBatchStart)
.Take(BatchSize)
.ToArray();
var topologyResults =
new ConcurrentBag<CollisionPayloadResult>();
Parallel.ForEach(
topologyBatch,
new ParallelOptions
{
MaxDegreeOfParallelism = options.Threads,
CancellationToken = cancellationToken,
},
fileId =>
{
try
{
if (!datReaderWriter.Cell.TryGet<EnvCell>(
fileId,
out EnvCell? envCell)
|| envCell is null)
{
failures.Add((
PakAssetType.EnvCellTopology,
fileId,
"EnvCell DAT record was not found"));
return;
}
FlatEnvCellTopology topology =
FlatCollisionAssetBuilder
.FlattenEnvCellTopology(
fileId,
envCell,
result.Structure
.PortalPolygons);
byte[] payload =
FlatCollisionAssetSerializer.Serialize(
topology,
cancellationToken);
topologyResults.Add(
new CollisionPayloadResult(
PakKey.Compose(
PakAssetType.EnvCellTopology,
fileId),
PakAssetType.EnvCellTopology,
fileId,
payload));
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception exception)
{
failures.Add((
PakAssetType.EnvCellTopology,
fileId,
exception.Message));
}
finally
{
Interlocked.Increment(
ref collisionCompleted);
}
});
foreach (CollisionPayloadResult topology in
topologyResults.OrderBy(
result => result.Key))
{
writer.AddBlob(
topology.Key,
topology.Payload);
writtenKeys.Add(topology.Key);
envCellTopologyWritten++;
}
ReportProgressIfDue(
collisionCompleted,
totalCollisionJobs,
failures.Count,
collisionStopwatch.Elapsed,
lastProgressReport,
final: false);
}
}
}
collisionStopwatch.Stop();
ReportProgressIfDue(
collisionCompleted,
totalCollisionJobs,
failures.Count,
collisionStopwatch.Elapsed,
lastProgressReport,
final: true);
writer.Finish();
stopwatch.Stop();
var typeCounts = new Dictionary<PakAssetType, int>
{
[PakAssetType.GfxObjMesh] =
gfxObjWritten + sideStagedWritten,
[PakAssetType.SetupMesh] = setupWritten,
[PakAssetType.EnvCellMesh] = envCellKeysWritten,
[PakAssetType.GfxObjCollision] = gfxCollisionWritten,
[PakAssetType.SetupCollision] = setupCollisionWritten,
[PakAssetType.CellStructureCollision] =
cellStructureWritten,
[PakAssetType.EnvCellTopology] = envCellTopologyWritten,
};
var report = new BakeReport
{
Header = header,
GfxObjKeys = gfxObjWritten,
SetupKeys = setupWritten,
EnvCellKeys = envCellKeysWritten,
UniqueEnvCellGeometries = envCellGeometriesWritten,
EnvCellAliases = envCellKeysWritten - envCellGeometriesWritten,
GfxObjCollisionKeys = gfxCollisionWritten,
UniqueGfxObjCollisions = uniqueGfxCollisions,
GfxObjCollisionAliases = gfxCollisionAliasCount,
SetupCollisionKeys = setupCollisionWritten,
UniqueSetupCollisions = uniqueSetupCollisions,
SetupCollisionAliases = setupCollisionAliasCount,
CellStructureCollisionKeys = cellStructureWritten,
UniqueCellStructureCollisions = uniqueCellStructures,
CellStructureCollisionAliases =
cellStructureAliasCount,
EnvCellTopologyKeys = envCellTopologyWritten,
SideStagedKeys = sideStagedWritten,
PhysicalBlobs =
gfxObjWritten
+ setupWritten
+ envCellGeometriesWritten
+ sideStagedWritten
+ uniqueGfxCollisions
+ uniqueSetupCollisions
+ uniqueCellStructures
+ envCellTopologyWritten,
TotalKeys = writtenKeys.Count,
Failures = failures.Count,
ExtractionAndWriteElapsed = stopwatch.Elapsed,
Elapsed = stopwatch.Elapsed,
OutputBytes = new FileInfo(options.OutPath).Length,
PeakWorkingSetBytes = Process.GetCurrentProcess().PeakWorkingSet64,
PeakPrivateBytes = Process.GetCurrentProcess().PeakPagedMemorySize64,
TypeCounts = typeCounts,
};
report = report with { SideStagedDuplicateKeys = sideStagedDuped };
PrintFailures(failures);
return report;
}
}
private static bool WriteExactPayload(
PakWriter writer,
ulong key,
byte[] payload,
ExactPayloadAliasCatalog aliases)
{
if (aliases.TryFind(payload, out ulong primaryKey))
{
writer.AddAlias(key, primaryKey);
return true;
}
writer.AddBlob(key, payload);
aliases.Add(key, payload);
return false;
}
private static (int Physical, int Aliases) WriteExactPayloadGroup(
PakWriter writer,
PakAssetType type,
IReadOnlyList<uint> fileIds,
byte[] payload,
ExactPayloadAliasCatalog aliases,
HashSet<ulong> writtenKeys)
{
if (fileIds.Count == 0)
throw new InvalidDataException("collision alias group is empty");
int firstUnwrittenIndex = 0;
ulong primaryKey;
int physical;
if (aliases.TryFind(payload, out primaryKey))
{
physical = 0;
}
else
{
primaryKey = PakKey.Compose(type, fileIds[0]);
writer.AddBlob(primaryKey, payload);
aliases.Add(primaryKey, payload);
writtenKeys.Add(primaryKey);
firstUnwrittenIndex = 1;
physical = 1;
}
int aliasCount = 0;
for (int i = firstUnwrittenIndex; i < fileIds.Count; i++)
{
ulong aliasKey = PakKey.Compose(type, fileIds[i]);
writer.AddAlias(aliasKey, primaryKey);
writtenKeys.Add(aliasKey);
aliasCount++;
}
return (physical, aliasCount);
}
private static int DrainSideStaged(
ConcurrentQueue<ObjectMeshData> sideStaged,
Dictionary<ulong, ObjectMeshData> sideStagedByKey,
HashSet<ulong> writtenKeys)
{
int duplicates = 0;
while (sideStaged.TryDequeue(out var staged))
{
uint fileId = (uint)(staged.ObjectId & 0xFFFF_FFFFu);
ulong key = PakKey.Compose(PakAssetType.GfxObjMesh, fileId);
if (writtenKeys.Contains(key) || !sideStagedByKey.TryAdd(key, staged))
duplicates++;
}
return duplicates;
}
private static void ReportProgressIfDue(
long done,
int total,
int failures,
TimeSpan elapsed,
Stopwatch lastProgressReport,
bool final)
{
if (!final && lastProgressReport.Elapsed.TotalSeconds < 5)
return;
double rate = elapsed.TotalSeconds > 0 ? done / elapsed.TotalSeconds : 0;
double etaSeconds = rate > 0 ? (total - done) / rate : 0;
using var process = Process.GetCurrentProcess();
process.Refresh();
long managedHeap = GC.GetGCMemoryInfo().HeapSizeBytes;
Console.WriteLine(
$"[{elapsed:hh\\:mm\\:ss}] extracted {done:N0}/{total:N0}, " +
$"failures={failures:N0}, elapsed={elapsed.TotalSeconds:F0}s, " +
$"ETA={etaSeconds:F0}s, private={process.PrivateMemorySize64 / 1024.0 / 1024.0:F0}MB, " +
$"managed={managedHeap / 1024.0 / 1024.0:F0}MB");
lastProgressReport.Restart();
}
private static void PrintSummary(BakeReport report, string outputPath)
{
Console.WriteLine();
Console.WriteLine("=== bake summary ===");
Console.WriteLine($" GfxObj keys: {report.GfxObjKeys:N0}");
Console.WriteLine($" Setup keys: {report.SetupKeys:N0}");
Console.WriteLine($" EnvCell keys: {report.EnvCellKeys:N0}");
Console.WriteLine($" EnvCell unique: {report.UniqueEnvCellGeometries:N0}");
Console.WriteLine($" EnvCell aliases: {report.EnvCellAliases:N0}");
Console.WriteLine($" EnvCell dedup: {report.EnvCellDedupRatio:F1}x");
Console.WriteLine(
$" Gfx collisions: {report.GfxObjCollisionKeys:N0} keys, " +
$"{report.UniqueGfxObjCollisions:N0} blobs, " +
$"{report.GfxObjCollisionAliases:N0} aliases");
Console.WriteLine(
$" Setup collisions: {report.SetupCollisionKeys:N0} keys, " +
$"{report.UniqueSetupCollisions:N0} blobs, " +
$"{report.SetupCollisionAliases:N0} aliases");
Console.WriteLine(
$" Cell structures: {report.CellStructureCollisionKeys:N0} keys, " +
$"{report.UniqueCellStructureCollisions:N0} blobs, " +
$"{report.CellStructureCollisionAliases:N0} aliases");
Console.WriteLine(
$" Cell topologies: {report.EnvCellTopologyKeys:N0}");
Console.WriteLine(
$" side-staged keys: {report.SideStagedKeys:N0} " +
$"({report.SideStagedDuplicateKeys:N0} duplicate keys)");
Console.WriteLine($" physical blobs: {report.PhysicalBlobs:N0}");
Console.WriteLine($" total keys: {report.TotalKeys:N0}");
Console.WriteLine($" failures: {report.Failures:N0}");
Console.WriteLine(
$" extract + write: {report.ExtractionAndWriteElapsed.TotalSeconds:F1} s");
Console.WriteLine($" total validated: {report.Elapsed.TotalSeconds:F1} s");
Console.WriteLine(
$" peak working set: {report.PeakWorkingSetBytes / 1024.0 / 1024.0:F1} MB");
Console.WriteLine(
$" peak private: {report.PeakPrivateBytes / 1024.0 / 1024.0:F1} MB");
Console.WriteLine($" output size: {report.OutputBytes / 1024.0 / 1024.0:F1} MB");
Console.WriteLine($" output path: {outputPath}");
}
private static void PrintFailures(
ConcurrentBag<(PakAssetType Type, uint FileId, string Reason)> failures)
{
if (failures.IsEmpty)
return;
Console.WriteLine();
Console.WriteLine($"failures ({failures.Count}):");
foreach (var (type, fileId, reason) in failures
.OrderBy(failure => failure.Type)
.ThenBy(failure => failure.FileId)
.Take(200))
{
Console.WriteLine($" {type,-12} 0x{fileId:X8}: {reason}");
}
if (failures.Count > 200)
Console.WriteLine($" ... and {failures.Count - 200} more");
}
/// <summary>
/// Enumerates EnvCell IDs by walking each cell DAT LandBlockInfo record's
/// NumCells range. DatReaderWriter cannot enumerate these range-shaped IDs
/// through GetAllIdsOfType.
/// </summary>
private static List<uint> EnumerateEnvCellIds(
DatCollection dats,
HashSet<uint>? landblockFilter)
{
var landblockInfoIds = new List<uint>();
foreach (var file in dats.Cell.Tree)
{
if ((file.Id & 0xFFFFu) != 0xFFFEu)
continue;
uint landblockId = file.Id & 0xFFFF_0000u;
if (landblockFilter is not null && !landblockFilter.Contains(landblockId))
continue;
landblockInfoIds.Add(file.Id);
}
landblockInfoIds.Sort();
var envCellIds = new List<uint>();
foreach (uint landblockInfoId in landblockInfoIds)
{
if (!dats.Cell.TryGet<LandBlockInfo>(landblockInfoId, out var info) ||
info is null ||
info.NumCells == 0)
{
continue;
}
uint firstCellId = (landblockInfoId & 0xFFFF_0000u) | 0x0100u;
for (uint offset = 0; offset < info.NumCells; offset++)
envCellIds.Add(firstCellId + offset);
}
return envCellIds;
}
private sealed record CollisionPayloadResult(
ulong Key,
PakAssetType Type,
uint FileId,
byte[] Payload);
private sealed record CellStructurePayloadResult(
EnvCellGeometryGroup Group,
FlatCellStructureCollisionAsset Structure,
byte[] Payload);
}