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.
This commit is contained in:
parent
d9300c7854
commit
9cd42417a8
29 changed files with 2868 additions and 63 deletions
|
|
@ -10,6 +10,10 @@
|
|||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="AcDream.Bake.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- NO Silk.NET / GL packages here — ever. acdream-bake is an offline CLI
|
||||
that drives the GL-free AcDream.Content.MeshExtractor; it must not
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ public static class BakeArtifactValidator
|
|||
public static void Validate(
|
||||
string path,
|
||||
in PakHeader expectedHeader,
|
||||
int expectedTocCount)
|
||||
int expectedTocCount,
|
||||
IReadOnlyDictionary<PakAssetType, int>? expectedTypeCounts = null)
|
||||
{
|
||||
using var reader = new PakReader(path);
|
||||
var actual = reader.Header;
|
||||
|
|
@ -35,5 +36,18 @@ public static class BakeArtifactValidator
|
|||
$"{expectedTocCount}");
|
||||
|
||||
reader.ValidateTocStructure();
|
||||
if (expectedTypeCounts is null)
|
||||
return;
|
||||
|
||||
foreach ((PakAssetType type, int expectedCount) in expectedTypeCounts)
|
||||
{
|
||||
int actualCount = reader.CountEntries(type);
|
||||
if (actualCount != expectedCount)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"bake catalog type {type} contains {actualCount} keys; " +
|
||||
$"expected {expectedCount}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ 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;
|
||||
|
|
@ -30,6 +33,16 @@ public sealed record BakeReport
|
|||
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; }
|
||||
|
|
@ -40,6 +53,11 @@ public sealed record BakeReport
|
|||
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;
|
||||
|
|
@ -80,7 +98,8 @@ public static class BakeRunner
|
|||
(temporaryPath, result) => BakeArtifactValidator.Validate(
|
||||
temporaryPath,
|
||||
result.Header,
|
||||
result.TotalKeys),
|
||||
result.TotalKeys,
|
||||
result.TypeCounts),
|
||||
options.CancellationToken);
|
||||
totalStopwatch.Stop();
|
||||
|
||||
|
|
@ -371,9 +390,402 @@ public static class BakeRunner
|
|||
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,
|
||||
|
|
@ -382,9 +794,27 @@ public static class BakeRunner
|
|||
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,
|
||||
gfxObjWritten
|
||||
+ setupWritten
|
||||
+ envCellGeometriesWritten
|
||||
+ sideStagedWritten
|
||||
+ uniqueGfxCollisions
|
||||
+ uniqueSetupCollisions
|
||||
+ uniqueCellStructures
|
||||
+ envCellTopologyWritten,
|
||||
TotalKeys = writtenKeys.Count,
|
||||
Failures = failures.Count,
|
||||
ExtractionAndWriteElapsed = stopwatch.Elapsed,
|
||||
|
|
@ -392,6 +822,7 @@ public static class BakeRunner
|
|||
OutputBytes = new FileInfo(options.OutPath).Length,
|
||||
PeakWorkingSetBytes = Process.GetCurrentProcess().PeakWorkingSet64,
|
||||
PeakPrivateBytes = Process.GetCurrentProcess().PeakPagedMemorySize64,
|
||||
TypeCounts = typeCounts,
|
||||
};
|
||||
|
||||
report = report with { SideStagedDuplicateKeys = sideStagedDuped };
|
||||
|
|
@ -400,6 +831,63 @@ public static class BakeRunner
|
|||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
|
@ -450,6 +938,20 @@ public static class BakeRunner
|
|||
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)");
|
||||
|
|
@ -526,4 +1028,15 @@ public static class BakeRunner
|
|||
|
||||
return envCellIds;
|
||||
}
|
||||
|
||||
private sealed record CollisionPayloadResult(
|
||||
ulong Key,
|
||||
PakAssetType Type,
|
||||
uint FileId,
|
||||
byte[] Payload);
|
||||
|
||||
private sealed record CellStructurePayloadResult(
|
||||
EnvCellGeometryGroup Group,
|
||||
FlatCellStructureCollisionAsset Structure,
|
||||
byte[] Payload);
|
||||
}
|
||||
|
|
|
|||
50
src/AcDream.Bake/ExactPayloadAliasCatalog.cs
Normal file
50
src/AcDream.Bake/ExactPayloadAliasCatalog.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
using System.Security.Cryptography;
|
||||
|
||||
namespace AcDream.Bake;
|
||||
|
||||
/// <summary>
|
||||
/// Offline-only exact-byte deduplication catalog. SHA-256 narrows candidates;
|
||||
/// a complete byte comparison is still mandatory before any alias is emitted.
|
||||
/// </summary>
|
||||
internal sealed class ExactPayloadAliasCatalog
|
||||
{
|
||||
private readonly Dictionary<string, List<Entry>> _entriesByDigest =
|
||||
new(StringComparer.Ordinal);
|
||||
|
||||
public bool TryFind(ReadOnlySpan<byte> payload, out ulong primaryKey)
|
||||
{
|
||||
string digest = Digest(payload);
|
||||
if (_entriesByDigest.TryGetValue(digest, out List<Entry>? candidates))
|
||||
{
|
||||
foreach (Entry candidate in candidates)
|
||||
{
|
||||
if (payload.SequenceEqual(candidate.Payload))
|
||||
{
|
||||
primaryKey = candidate.PrimaryKey;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
primaryKey = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Add(ulong primaryKey, byte[] payload)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(payload);
|
||||
string digest = Digest(payload);
|
||||
if (!_entriesByDigest.TryGetValue(digest, out List<Entry>? entries))
|
||||
{
|
||||
entries = [];
|
||||
_entriesByDigest.Add(digest, entries);
|
||||
}
|
||||
|
||||
entries.Add(new Entry(primaryKey, payload));
|
||||
}
|
||||
|
||||
private static string Digest(ReadOnlySpan<byte> payload) =>
|
||||
Convert.ToHexString(SHA256.HashData(payload));
|
||||
|
||||
private sealed record Entry(ulong PrimaryKey, byte[] Payload);
|
||||
}
|
||||
715
src/AcDream.Content/FlatCollisionAssetSerializer.cs
Normal file
715
src/AcDream.Content/FlatCollisionAssetSerializer.cs
Normal file
|
|
@ -0,0 +1,715 @@
|
|||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Immutable;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using DatReaderWriter.Enums;
|
||||
|
||||
namespace AcDream.Content;
|
||||
|
||||
/// <summary>
|
||||
/// Strict little-endian package codec for immutable flat collision assets.
|
||||
/// Every float is persisted through its exact IEEE-754 bit pattern. Readers
|
||||
/// reject invalid counts, invalid enum/range/tree contracts, wrong payload
|
||||
/// kinds, non-zero reserved bytes, truncation, and trailing bytes.
|
||||
/// </summary>
|
||||
public static class FlatCollisionAssetSerializer
|
||||
{
|
||||
private const uint Magic = 0x4C_43_43_41u; // "ACCL" little-endian
|
||||
private const byte SchemaVersion = 1;
|
||||
private const int MaximumRows = 16_777_216;
|
||||
|
||||
public static byte[] Serialize(
|
||||
FlatGfxObjCollisionAsset asset,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(asset);
|
||||
var writer = new CollisionWriter();
|
||||
writer.WriteHeader(CollisionPayloadKind.GfxObj);
|
||||
WritePhysicsBsp(writer, asset.PhysicsBsp, cancellationToken);
|
||||
writer.WriteOptionalSphere(asset.BoundingSphere);
|
||||
writer.WriteBoolean(asset.VisualBounds.HasValue);
|
||||
if (asset.VisualBounds is FlatGfxObjVisualBounds visual)
|
||||
{
|
||||
writer.WriteVector3(visual.Min);
|
||||
writer.WriteVector3(visual.Max);
|
||||
writer.WriteVector3(visual.Center);
|
||||
writer.WriteSingle(visual.Radius);
|
||||
writer.WriteVector3(visual.HalfExtents);
|
||||
}
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return writer.ToArray();
|
||||
}
|
||||
|
||||
public static byte[] Serialize(
|
||||
FlatSetupCollision asset,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(asset);
|
||||
var writer = new CollisionWriter();
|
||||
writer.WriteHeader(CollisionPayloadKind.Setup);
|
||||
writer.WriteCount(asset.Cylinders.Length, "Setup cylinder");
|
||||
writer.WriteCount(asset.Spheres.Length, "Setup sphere");
|
||||
writer.WriteSingle(asset.Height);
|
||||
writer.WriteSingle(asset.Radius);
|
||||
writer.WriteSingle(asset.StepUpHeight);
|
||||
writer.WriteSingle(asset.StepDownHeight);
|
||||
|
||||
for (int i = 0; i < asset.Cylinders.Length; i++)
|
||||
{
|
||||
CheckCancellation(cancellationToken, i);
|
||||
FlatCollisionCylinder cylinder = asset.Cylinders[i];
|
||||
writer.WriteVector3(cylinder.Origin);
|
||||
writer.WriteSingle(cylinder.Radius);
|
||||
writer.WriteSingle(cylinder.Height);
|
||||
}
|
||||
for (int i = 0; i < asset.Spheres.Length; i++)
|
||||
{
|
||||
CheckCancellation(cancellationToken, i);
|
||||
writer.WriteSphere(asset.Spheres[i]);
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return writer.ToArray();
|
||||
}
|
||||
|
||||
public static byte[] Serialize(
|
||||
FlatCellStructureCollisionAsset asset,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(asset);
|
||||
var writer = new CollisionWriter();
|
||||
writer.WriteHeader(CollisionPayloadKind.CellStructure);
|
||||
WritePhysicsBsp(writer, asset.PhysicsBsp, cancellationToken);
|
||||
WriteContainmentBsp(writer, asset.ContainmentBsp, cancellationToken);
|
||||
WritePolygonTable(writer, asset.PortalPolygons, cancellationToken);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return writer.ToArray();
|
||||
}
|
||||
|
||||
public static byte[] Serialize(
|
||||
FlatEnvCellTopology asset,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(asset);
|
||||
var writer = new CollisionWriter();
|
||||
writer.WriteHeader(CollisionPayloadKind.EnvCellTopology);
|
||||
writer.WriteCount(asset.Portals.Length, "EnvCell portal");
|
||||
writer.WriteCount(asset.VisibleCellIds.Length, "visible-cell");
|
||||
writer.WriteBoolean(asset.SeenOutside);
|
||||
|
||||
for (int i = 0; i < asset.Portals.Length; i++)
|
||||
{
|
||||
CheckCancellation(cancellationToken, i);
|
||||
FlatEnvCellPortal portal = asset.Portals[i];
|
||||
writer.WriteUInt16(portal.OtherCellId);
|
||||
writer.WriteUInt16(portal.PolygonId);
|
||||
writer.WriteUInt16(portal.Flags);
|
||||
writer.WriteInt32(portal.PolygonIndex);
|
||||
}
|
||||
for (int i = 0; i < asset.VisibleCellIds.Length; i++)
|
||||
{
|
||||
CheckCancellation(cancellationToken, i);
|
||||
writer.WriteUInt32(asset.VisibleCellIds[i]);
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return writer.ToArray();
|
||||
}
|
||||
|
||||
public static FlatGfxObjCollisionAsset DeserializeGfxObj(
|
||||
ReadOnlySpan<byte> bytes,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var reader = new CollisionReader(bytes, cancellationToken);
|
||||
reader.ReadHeader(CollisionPayloadKind.GfxObj);
|
||||
FlatPhysicsBsp physicsBsp = ReadPhysicsBsp(ref reader);
|
||||
FlatCollisionSphere? boundingSphere = reader.ReadOptionalSphere();
|
||||
FlatGfxObjVisualBounds? visualBounds = null;
|
||||
if (reader.ReadBoolean())
|
||||
{
|
||||
visualBounds = new FlatGfxObjVisualBounds(
|
||||
reader.ReadVector3(),
|
||||
reader.ReadVector3(),
|
||||
reader.ReadVector3(),
|
||||
reader.ReadSingle(),
|
||||
reader.ReadVector3());
|
||||
}
|
||||
reader.RequireEnd();
|
||||
return new FlatGfxObjCollisionAsset(
|
||||
physicsBsp,
|
||||
boundingSphere,
|
||||
visualBounds);
|
||||
}
|
||||
|
||||
public static FlatSetupCollision DeserializeSetup(
|
||||
ReadOnlySpan<byte> bytes,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var reader = new CollisionReader(bytes, cancellationToken);
|
||||
reader.ReadHeader(CollisionPayloadKind.Setup);
|
||||
int cylinderCount = reader.ReadCount(
|
||||
"Setup cylinder",
|
||||
bytesPerItem: 20);
|
||||
int sphereCount = reader.ReadCount(
|
||||
"Setup sphere",
|
||||
bytesPerItem: 16);
|
||||
reader.RequireRemaining(
|
||||
checked(16L + (20L * cylinderCount) + (16L * sphereCount)),
|
||||
"Setup collision rows");
|
||||
float height = reader.ReadSingle();
|
||||
float radius = reader.ReadSingle();
|
||||
float stepUpHeight = reader.ReadSingle();
|
||||
float stepDownHeight = reader.ReadSingle();
|
||||
|
||||
var cylinders =
|
||||
ImmutableArray.CreateBuilder<FlatCollisionCylinder>(cylinderCount);
|
||||
for (int i = 0; i < cylinderCount; i++)
|
||||
{
|
||||
reader.CheckCancellation(i);
|
||||
cylinders.Add(new FlatCollisionCylinder(
|
||||
reader.ReadVector3(),
|
||||
reader.ReadSingle(),
|
||||
reader.ReadSingle()));
|
||||
}
|
||||
|
||||
var spheres =
|
||||
ImmutableArray.CreateBuilder<FlatCollisionSphere>(sphereCount);
|
||||
for (int i = 0; i < sphereCount; i++)
|
||||
{
|
||||
reader.CheckCancellation(i);
|
||||
spheres.Add(reader.ReadSphere());
|
||||
}
|
||||
|
||||
reader.RequireEnd();
|
||||
return new FlatSetupCollision(
|
||||
cylinders.MoveToImmutable(),
|
||||
spheres.MoveToImmutable(),
|
||||
height,
|
||||
radius,
|
||||
stepUpHeight,
|
||||
stepDownHeight);
|
||||
}
|
||||
|
||||
public static FlatCellStructureCollisionAsset DeserializeCellStructure(
|
||||
ReadOnlySpan<byte> bytes,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var reader = new CollisionReader(bytes, cancellationToken);
|
||||
reader.ReadHeader(CollisionPayloadKind.CellStructure);
|
||||
FlatPhysicsBsp physicsBsp = ReadPhysicsBsp(ref reader);
|
||||
FlatCellContainmentBsp containmentBsp = ReadContainmentBsp(ref reader);
|
||||
FlatPolygonTable portalPolygons = ReadPolygonTable(ref reader);
|
||||
reader.RequireEnd();
|
||||
return new FlatCellStructureCollisionAsset(
|
||||
physicsBsp,
|
||||
containmentBsp,
|
||||
portalPolygons);
|
||||
}
|
||||
|
||||
public static FlatEnvCellTopology DeserializeEnvCellTopology(
|
||||
ReadOnlySpan<byte> bytes,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var reader = new CollisionReader(bytes, cancellationToken);
|
||||
reader.ReadHeader(CollisionPayloadKind.EnvCellTopology);
|
||||
int portalCount = reader.ReadCount(
|
||||
"EnvCell portal",
|
||||
bytesPerItem: 10);
|
||||
int visibleCount = reader.ReadCount(
|
||||
"visible-cell",
|
||||
bytesPerItem: 4);
|
||||
reader.RequireRemaining(
|
||||
checked(1L + (10L * portalCount) + (4L * visibleCount)),
|
||||
"EnvCell topology rows");
|
||||
bool seenOutside = reader.ReadBoolean();
|
||||
|
||||
var portals =
|
||||
ImmutableArray.CreateBuilder<FlatEnvCellPortal>(portalCount);
|
||||
for (int i = 0; i < portalCount; i++)
|
||||
{
|
||||
reader.CheckCancellation(i);
|
||||
portals.Add(new FlatEnvCellPortal(
|
||||
reader.ReadUInt16(),
|
||||
reader.ReadUInt16(),
|
||||
reader.ReadUInt16(),
|
||||
reader.ReadInt32()));
|
||||
}
|
||||
|
||||
var visible =
|
||||
ImmutableArray.CreateBuilder<uint>(visibleCount);
|
||||
for (int i = 0; i < visibleCount; i++)
|
||||
{
|
||||
reader.CheckCancellation(i);
|
||||
visible.Add(reader.ReadUInt32());
|
||||
}
|
||||
|
||||
reader.RequireEnd();
|
||||
return new FlatEnvCellTopology(
|
||||
portals.MoveToImmutable(),
|
||||
visible.MoveToImmutable(),
|
||||
seenOutside);
|
||||
}
|
||||
|
||||
private static void WritePhysicsBsp(
|
||||
CollisionWriter writer,
|
||||
FlatPhysicsBsp bsp,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bsp);
|
||||
writer.WriteInt32(bsp.RootIndex);
|
||||
writer.WriteCount(bsp.Nodes.Length, "physics BSP node");
|
||||
writer.WriteCount(
|
||||
bsp.PolygonIndexStream.Length,
|
||||
"physics BSP polygon-index");
|
||||
|
||||
for (int i = 0; i < bsp.Nodes.Length; i++)
|
||||
{
|
||||
CheckCancellation(cancellationToken, i);
|
||||
FlatPhysicsBspNode node = bsp.Nodes[i];
|
||||
writer.WriteUInt32((uint)node.Type);
|
||||
writer.WritePlane(node.SplittingPlane);
|
||||
writer.WriteInt32(node.PositiveChildIndex);
|
||||
writer.WriteInt32(node.NegativeChildIndex);
|
||||
writer.WriteInt32(node.LeafIndex);
|
||||
writer.WriteInt32(node.Solid);
|
||||
writer.WriteSphere(node.BoundingSphere);
|
||||
writer.WriteRange(node.PolygonIndexRange);
|
||||
}
|
||||
|
||||
for (int i = 0; i < bsp.PolygonIndexStream.Length; i++)
|
||||
{
|
||||
CheckCancellation(cancellationToken, i);
|
||||
writer.WriteInt32(bsp.PolygonIndexStream[i]);
|
||||
}
|
||||
WritePolygonTable(writer, bsp.PolygonTable, cancellationToken);
|
||||
}
|
||||
|
||||
private static FlatPhysicsBsp ReadPhysicsBsp(
|
||||
ref CollisionReader reader)
|
||||
{
|
||||
int rootIndex = reader.ReadInt32();
|
||||
int nodeCount = reader.ReadCount(
|
||||
"physics BSP node",
|
||||
bytesPerItem: 60);
|
||||
int polygonIndexCount = reader.ReadCount(
|
||||
"physics BSP polygon-index",
|
||||
bytesPerItem: 4);
|
||||
reader.RequireRemaining(
|
||||
checked((60L * nodeCount) + (4L * polygonIndexCount) + 8L),
|
||||
"physics BSP rows");
|
||||
|
||||
var nodes =
|
||||
ImmutableArray.CreateBuilder<FlatPhysicsBspNode>(nodeCount);
|
||||
for (int i = 0; i < nodeCount; i++)
|
||||
{
|
||||
reader.CheckCancellation(i);
|
||||
nodes.Add(new FlatPhysicsBspNode(
|
||||
(BSPNodeType)reader.ReadUInt32(),
|
||||
reader.ReadPlane(),
|
||||
reader.ReadInt32(),
|
||||
reader.ReadInt32(),
|
||||
reader.ReadInt32(),
|
||||
reader.ReadInt32(),
|
||||
reader.ReadSphere(),
|
||||
reader.ReadRange()));
|
||||
}
|
||||
|
||||
var polygonIndices =
|
||||
ImmutableArray.CreateBuilder<int>(polygonIndexCount);
|
||||
for (int i = 0; i < polygonIndexCount; i++)
|
||||
{
|
||||
reader.CheckCancellation(i);
|
||||
polygonIndices.Add(reader.ReadInt32());
|
||||
}
|
||||
|
||||
FlatPolygonTable polygons = ReadPolygonTable(ref reader);
|
||||
return new FlatPhysicsBsp(
|
||||
rootIndex,
|
||||
nodes.MoveToImmutable(),
|
||||
polygonIndices.MoveToImmutable(),
|
||||
polygons);
|
||||
}
|
||||
|
||||
private static void WriteContainmentBsp(
|
||||
CollisionWriter writer,
|
||||
FlatCellContainmentBsp bsp,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bsp);
|
||||
writer.WriteInt32(bsp.RootIndex);
|
||||
writer.WriteCount(bsp.Nodes.Length, "cell-containment BSP node");
|
||||
for (int i = 0; i < bsp.Nodes.Length; i++)
|
||||
{
|
||||
CheckCancellation(cancellationToken, i);
|
||||
FlatCellBspNode node = bsp.Nodes[i];
|
||||
writer.WriteUInt32((uint)node.Type);
|
||||
writer.WritePlane(node.SplittingPlane);
|
||||
writer.WriteInt32(node.PositiveChildIndex);
|
||||
writer.WriteInt32(node.NegativeChildIndex);
|
||||
writer.WriteInt32(node.LeafIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private static FlatCellContainmentBsp ReadContainmentBsp(
|
||||
ref CollisionReader reader)
|
||||
{
|
||||
int rootIndex = reader.ReadInt32();
|
||||
int nodeCount = reader.ReadCount(
|
||||
"cell-containment BSP node",
|
||||
bytesPerItem: 32);
|
||||
var nodes =
|
||||
ImmutableArray.CreateBuilder<FlatCellBspNode>(nodeCount);
|
||||
for (int i = 0; i < nodeCount; i++)
|
||||
{
|
||||
reader.CheckCancellation(i);
|
||||
nodes.Add(new FlatCellBspNode(
|
||||
(BSPNodeType)reader.ReadUInt32(),
|
||||
reader.ReadPlane(),
|
||||
reader.ReadInt32(),
|
||||
reader.ReadInt32(),
|
||||
reader.ReadInt32()));
|
||||
}
|
||||
return new FlatCellContainmentBsp(
|
||||
rootIndex,
|
||||
nodes.MoveToImmutable());
|
||||
}
|
||||
|
||||
private static void WritePolygonTable(
|
||||
CollisionWriter writer,
|
||||
FlatPolygonTable table,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(table);
|
||||
writer.WriteCount(table.Polygons.Length, "polygon");
|
||||
writer.WriteCount(table.Vertices.Length, "polygon vertex");
|
||||
for (int i = 0; i < table.Polygons.Length; i++)
|
||||
{
|
||||
CheckCancellation(cancellationToken, i);
|
||||
FlatCollisionPolygon polygon = table.Polygons[i];
|
||||
writer.WriteUInt16(polygon.Id);
|
||||
writer.WriteUInt32((uint)polygon.SidesType);
|
||||
writer.WriteInt32(polygon.NumPoints);
|
||||
writer.WriteRange(polygon.VertexRange);
|
||||
writer.WritePlane(polygon.Plane);
|
||||
}
|
||||
for (int i = 0; i < table.Vertices.Length; i++)
|
||||
{
|
||||
CheckCancellation(cancellationToken, i);
|
||||
writer.WriteVector3(table.Vertices[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static FlatPolygonTable ReadPolygonTable(
|
||||
ref CollisionReader reader)
|
||||
{
|
||||
int polygonCount = reader.ReadCount(
|
||||
"polygon",
|
||||
bytesPerItem: 34);
|
||||
int vertexCount = reader.ReadCount(
|
||||
"polygon vertex",
|
||||
bytesPerItem: 12);
|
||||
reader.RequireRemaining(
|
||||
checked((34L * polygonCount) + (12L * vertexCount)),
|
||||
"polygon-table rows");
|
||||
var polygons =
|
||||
ImmutableArray.CreateBuilder<FlatCollisionPolygon>(polygonCount);
|
||||
for (int i = 0; i < polygonCount; i++)
|
||||
{
|
||||
reader.CheckCancellation(i);
|
||||
polygons.Add(new FlatCollisionPolygon(
|
||||
reader.ReadUInt16(),
|
||||
reader.ReadPlaneAfterMetadata(out CullMode sides, out int count,
|
||||
out FlatIndexRange range),
|
||||
sides,
|
||||
count,
|
||||
range));
|
||||
}
|
||||
|
||||
var vertices = ImmutableArray.CreateBuilder<Vector3>(vertexCount);
|
||||
for (int i = 0; i < vertexCount; i++)
|
||||
{
|
||||
reader.CheckCancellation(i);
|
||||
vertices.Add(reader.ReadVector3());
|
||||
}
|
||||
return new FlatPolygonTable(
|
||||
polygons.MoveToImmutable(),
|
||||
vertices.MoveToImmutable());
|
||||
}
|
||||
|
||||
private static void CheckCancellation(
|
||||
CancellationToken cancellationToken,
|
||||
int index)
|
||||
{
|
||||
if ((index & 0xFF) == 0)
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
private enum CollisionPayloadKind : byte
|
||||
{
|
||||
GfxObj = 1,
|
||||
Setup = 2,
|
||||
CellStructure = 3,
|
||||
EnvCellTopology = 4,
|
||||
}
|
||||
|
||||
private sealed class CollisionWriter
|
||||
{
|
||||
private readonly ArrayBufferWriter<byte> _buffer = new(256);
|
||||
|
||||
public void WriteHeader(CollisionPayloadKind kind)
|
||||
{
|
||||
WriteUInt32(Magic);
|
||||
WriteByte((byte)kind);
|
||||
WriteByte(SchemaVersion);
|
||||
WriteUInt16(0);
|
||||
}
|
||||
|
||||
public void WriteCount(int value, string name)
|
||||
{
|
||||
if ((uint)value > MaximumRows)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"{name} count {value} exceeds {MaximumRows}.");
|
||||
}
|
||||
WriteInt32(value);
|
||||
}
|
||||
|
||||
public void WriteOptionalSphere(FlatCollisionSphere? sphere)
|
||||
{
|
||||
WriteBoolean(sphere.HasValue);
|
||||
if (sphere.HasValue)
|
||||
WriteSphere(sphere.Value);
|
||||
}
|
||||
|
||||
public void WriteBoolean(bool value) => WriteByte(value ? (byte)1 : (byte)0);
|
||||
|
||||
public void WriteSphere(FlatCollisionSphere sphere)
|
||||
{
|
||||
WriteVector3(sphere.Origin);
|
||||
WriteSingle(sphere.Radius);
|
||||
}
|
||||
|
||||
public void WriteRange(FlatIndexRange range)
|
||||
{
|
||||
WriteInt32(range.Start);
|
||||
WriteInt32(range.Count);
|
||||
}
|
||||
|
||||
public void WritePlane(Plane plane)
|
||||
{
|
||||
WriteVector3(plane.Normal);
|
||||
WriteSingle(plane.D);
|
||||
}
|
||||
|
||||
public void WriteVector3(Vector3 vector)
|
||||
{
|
||||
WriteSingle(vector.X);
|
||||
WriteSingle(vector.Y);
|
||||
WriteSingle(vector.Z);
|
||||
}
|
||||
|
||||
public void WriteSingle(float value) =>
|
||||
WriteInt32(BitConverter.SingleToInt32Bits(value));
|
||||
|
||||
public void WriteByte(byte value)
|
||||
{
|
||||
Span<byte> span = _buffer.GetSpan(1);
|
||||
span[0] = value;
|
||||
_buffer.Advance(1);
|
||||
}
|
||||
|
||||
public void WriteUInt16(ushort value)
|
||||
{
|
||||
Span<byte> span = _buffer.GetSpan(sizeof(ushort));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(span, value);
|
||||
_buffer.Advance(sizeof(ushort));
|
||||
}
|
||||
|
||||
public void WriteInt32(int value)
|
||||
{
|
||||
Span<byte> span = _buffer.GetSpan(sizeof(int));
|
||||
BinaryPrimitives.WriteInt32LittleEndian(span, value);
|
||||
_buffer.Advance(sizeof(int));
|
||||
}
|
||||
|
||||
public void WriteUInt32(uint value)
|
||||
{
|
||||
Span<byte> span = _buffer.GetSpan(sizeof(uint));
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(span, value);
|
||||
_buffer.Advance(sizeof(uint));
|
||||
}
|
||||
|
||||
public byte[] ToArray() => _buffer.WrittenSpan.ToArray();
|
||||
}
|
||||
|
||||
private ref struct CollisionReader
|
||||
{
|
||||
private readonly ReadOnlySpan<byte> _bytes;
|
||||
private readonly CancellationToken _cancellationToken;
|
||||
private int _offset;
|
||||
|
||||
public CollisionReader(
|
||||
ReadOnlySpan<byte> bytes,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_bytes = bytes;
|
||||
_cancellationToken = cancellationToken;
|
||||
_offset = 0;
|
||||
_cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
private int Remaining => _bytes.Length - _offset;
|
||||
|
||||
public void ReadHeader(CollisionPayloadKind expectedKind)
|
||||
{
|
||||
uint magic = ReadUInt32();
|
||||
byte kind = ReadByte();
|
||||
byte version = ReadByte();
|
||||
ushort reserved = ReadUInt16();
|
||||
if (magic != Magic)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"collision payload magic 0x{magic:X8} does not match " +
|
||||
$"0x{Magic:X8}.");
|
||||
}
|
||||
if (kind != (byte)expectedKind)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"collision payload kind {kind} does not match " +
|
||||
$"{(byte)expectedKind}.");
|
||||
}
|
||||
if (version != SchemaVersion)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"collision payload schema {version} is unsupported.");
|
||||
}
|
||||
if (reserved != 0)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
"collision payload reserved header bits are non-zero.");
|
||||
}
|
||||
}
|
||||
|
||||
public int ReadCount(string name, int bytesPerItem)
|
||||
{
|
||||
int count = ReadInt32();
|
||||
if (count < 0 || count > MaximumRows)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"{name} count {count} is outside [0,{MaximumRows}].");
|
||||
}
|
||||
if (bytesPerItem <= 0 || count > Remaining / bytesPerItem)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"{name} count {count} exceeds the remaining payload.");
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public FlatCollisionSphere? ReadOptionalSphere() =>
|
||||
ReadBoolean() ? ReadSphere() : null;
|
||||
|
||||
public bool ReadBoolean()
|
||||
{
|
||||
byte value = ReadByte();
|
||||
return value switch
|
||||
{
|
||||
0 => false,
|
||||
1 => true,
|
||||
_ => throw new InvalidDataException(
|
||||
$"invalid collision boolean value {value}."),
|
||||
};
|
||||
}
|
||||
|
||||
public FlatCollisionSphere ReadSphere() =>
|
||||
new(ReadVector3(), ReadSingle());
|
||||
|
||||
public FlatIndexRange ReadRange() =>
|
||||
new(ReadInt32(), ReadInt32());
|
||||
|
||||
public Plane ReadPlane() =>
|
||||
new(ReadVector3(), ReadSingle());
|
||||
|
||||
public Plane ReadPlaneAfterMetadata(
|
||||
out CullMode sides,
|
||||
out int numPoints,
|
||||
out FlatIndexRange range)
|
||||
{
|
||||
sides = (CullMode)ReadUInt32();
|
||||
numPoints = ReadInt32();
|
||||
range = ReadRange();
|
||||
return ReadPlane();
|
||||
}
|
||||
|
||||
public Vector3 ReadVector3() =>
|
||||
new(ReadSingle(), ReadSingle(), ReadSingle());
|
||||
|
||||
public float ReadSingle() =>
|
||||
BitConverter.Int32BitsToSingle(ReadInt32());
|
||||
|
||||
public byte ReadByte()
|
||||
{
|
||||
Require(sizeof(byte));
|
||||
return _bytes[_offset++];
|
||||
}
|
||||
|
||||
public ushort ReadUInt16()
|
||||
{
|
||||
ReadOnlySpan<byte> span = Take(sizeof(ushort));
|
||||
return BinaryPrimitives.ReadUInt16LittleEndian(span);
|
||||
}
|
||||
|
||||
public int ReadInt32()
|
||||
{
|
||||
ReadOnlySpan<byte> span = Take(sizeof(int));
|
||||
return BinaryPrimitives.ReadInt32LittleEndian(span);
|
||||
}
|
||||
|
||||
public uint ReadUInt32()
|
||||
{
|
||||
ReadOnlySpan<byte> span = Take(sizeof(uint));
|
||||
return BinaryPrimitives.ReadUInt32LittleEndian(span);
|
||||
}
|
||||
|
||||
public void CheckCancellation(int index)
|
||||
{
|
||||
if ((index & 0xFF) == 0)
|
||||
_cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
public void RequireEnd()
|
||||
{
|
||||
_cancellationToken.ThrowIfCancellationRequested();
|
||||
if (_offset != _bytes.Length)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"collision payload contains {_bytes.Length - _offset} " +
|
||||
"trailing byte(s).");
|
||||
}
|
||||
}
|
||||
|
||||
public void RequireRemaining(long required, string name)
|
||||
{
|
||||
if (required < 0 || required > Remaining)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"{name} require {required} bytes but only {Remaining} remain.");
|
||||
}
|
||||
}
|
||||
|
||||
private ReadOnlySpan<byte> Take(int length)
|
||||
{
|
||||
Require(length);
|
||||
ReadOnlySpan<byte> result = _bytes.Slice(_offset, length);
|
||||
_offset += length;
|
||||
return result;
|
||||
}
|
||||
|
||||
private void Require(int length)
|
||||
{
|
||||
if (length < 0 || length > Remaining)
|
||||
throw new EndOfStreamException("collision payload is truncated.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -131,7 +131,10 @@ internal static class PreparedAssetRequestContract
|
|||
{
|
||||
public static void ValidateType(PakAssetType type)
|
||||
{
|
||||
if (!Enum.IsDefined(type))
|
||||
if (type is not (
|
||||
PakAssetType.GfxObjMesh or
|
||||
PakAssetType.SetupMesh or
|
||||
PakAssetType.EnvCellMesh))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(type),
|
||||
|
|
@ -166,7 +169,9 @@ internal static class PreparedAssetRequestContract
|
|||
/// Production prepared source backed by one immutable memory-mapped pak.
|
||||
/// There is intentionally no DAT fallback.
|
||||
/// </summary>
|
||||
public sealed class PakPreparedAssetSource : IPreparedAssetSource
|
||||
public sealed partial class PakPreparedAssetSource :
|
||||
IPreparedAssetSource,
|
||||
IPreparedCollisionSource
|
||||
{
|
||||
private readonly PakReader _reader;
|
||||
private readonly Action<string>? _diagnosticSink;
|
||||
|
|
|
|||
206
src/AcDream.Content/IPreparedCollisionSource.cs
Normal file
206
src/AcDream.Content/IPreparedCollisionSource.cs
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
using AcDream.Content.Pak;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.Content;
|
||||
|
||||
public readonly record struct PreparedCollisionSourceStats(
|
||||
long Probes,
|
||||
long Reads,
|
||||
long Loaded,
|
||||
long Missing,
|
||||
long Corrupt);
|
||||
|
||||
public readonly record struct PreparedCollisionReadResult<T>(
|
||||
PreparedAssetReadStatus Status,
|
||||
T? Data)
|
||||
where T : class
|
||||
{
|
||||
public static PreparedCollisionReadResult<T> Missing =>
|
||||
new(PreparedAssetReadStatus.Missing, null);
|
||||
|
||||
public static PreparedCollisionReadResult<T> Corrupt =>
|
||||
new(PreparedAssetReadStatus.Corrupt, null);
|
||||
|
||||
public static PreparedCollisionReadResult<T> Loaded(T data) =>
|
||||
new(PreparedAssetReadStatus.Loaded, data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Typed immutable collision payload source. It is deliberately separate from
|
||||
/// the render-only <see cref="IPreparedAssetSource"/> contract even when one
|
||||
/// concrete package owner implements both interfaces over the same mmap.
|
||||
/// </summary>
|
||||
public interface IPreparedCollisionSource : IDisposable
|
||||
{
|
||||
PreparedAssetPresence ProbeCollision(
|
||||
PakAssetType type,
|
||||
uint sourceFileId);
|
||||
|
||||
PreparedCollisionReadResult<FlatGfxObjCollisionAsset>
|
||||
ReadGfxObjCollision(
|
||||
uint sourceFileId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
PreparedCollisionReadResult<FlatSetupCollision>
|
||||
ReadSetupCollision(
|
||||
uint sourceFileId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
PreparedCollisionReadResult<FlatCellStructureCollisionAsset>
|
||||
ReadCellStructureCollision(
|
||||
uint sourceFileId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
PreparedCollisionReadResult<FlatEnvCellTopology>
|
||||
ReadEnvCellTopology(
|
||||
uint sourceFileId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
PreparedCollisionSourceStats CollisionStats { get; }
|
||||
}
|
||||
|
||||
internal static class PreparedCollisionTypeContract
|
||||
{
|
||||
public static void Validate(PakAssetType type)
|
||||
{
|
||||
if (type is not (
|
||||
PakAssetType.GfxObjCollision or
|
||||
PakAssetType.SetupCollision or
|
||||
PakAssetType.CellStructureCollision or
|
||||
PakAssetType.EnvCellTopology))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(type),
|
||||
type,
|
||||
"unknown prepared collision asset type");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed partial class PakPreparedAssetSource
|
||||
{
|
||||
private long _collisionProbes;
|
||||
private long _collisionReads;
|
||||
private long _collisionLoaded;
|
||||
private long _collisionMissing;
|
||||
private long _collisionCorrupt;
|
||||
|
||||
public PreparedCollisionSourceStats CollisionStats =>
|
||||
new(
|
||||
Volatile.Read(ref _collisionProbes),
|
||||
Volatile.Read(ref _collisionReads),
|
||||
Volatile.Read(ref _collisionLoaded),
|
||||
Volatile.Read(ref _collisionMissing),
|
||||
Volatile.Read(ref _collisionCorrupt));
|
||||
|
||||
public PreparedAssetPresence ProbeCollision(
|
||||
PakAssetType type,
|
||||
uint sourceFileId)
|
||||
{
|
||||
PreparedCollisionTypeContract.Validate(type);
|
||||
Interlocked.Increment(ref _collisionProbes);
|
||||
ulong key = PakKey.Compose(type, sourceFileId);
|
||||
return _reader.ProbeEntry(key) switch
|
||||
{
|
||||
PakEntryState.Available => PreparedAssetPresence.Available,
|
||||
PakEntryState.Corrupt => PreparedAssetPresence.Corrupt,
|
||||
_ => PreparedAssetPresence.Missing,
|
||||
};
|
||||
}
|
||||
|
||||
public PreparedCollisionReadResult<FlatGfxObjCollisionAsset>
|
||||
ReadGfxObjCollision(
|
||||
uint sourceFileId,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
ReadCollision(
|
||||
PakAssetType.GfxObjCollision,
|
||||
sourceFileId,
|
||||
static (bytes, token) =>
|
||||
FlatCollisionAssetSerializer.DeserializeGfxObj(bytes, token),
|
||||
cancellationToken);
|
||||
|
||||
public PreparedCollisionReadResult<FlatSetupCollision>
|
||||
ReadSetupCollision(
|
||||
uint sourceFileId,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
ReadCollision(
|
||||
PakAssetType.SetupCollision,
|
||||
sourceFileId,
|
||||
static (bytes, token) =>
|
||||
FlatCollisionAssetSerializer.DeserializeSetup(bytes, token),
|
||||
cancellationToken);
|
||||
|
||||
public PreparedCollisionReadResult<FlatCellStructureCollisionAsset>
|
||||
ReadCellStructureCollision(
|
||||
uint sourceFileId,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
ReadCollision(
|
||||
PakAssetType.CellStructureCollision,
|
||||
sourceFileId,
|
||||
static (bytes, token) =>
|
||||
FlatCollisionAssetSerializer.DeserializeCellStructure(
|
||||
bytes,
|
||||
token),
|
||||
cancellationToken);
|
||||
|
||||
public PreparedCollisionReadResult<FlatEnvCellTopology>
|
||||
ReadEnvCellTopology(
|
||||
uint sourceFileId,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
ReadCollision(
|
||||
PakAssetType.EnvCellTopology,
|
||||
sourceFileId,
|
||||
static (bytes, token) =>
|
||||
FlatCollisionAssetSerializer.DeserializeEnvCellTopology(
|
||||
bytes,
|
||||
token),
|
||||
cancellationToken);
|
||||
|
||||
private PreparedCollisionReadResult<T> ReadCollision<T>(
|
||||
PakAssetType type,
|
||||
uint sourceFileId,
|
||||
Func<byte[], CancellationToken, T> deserialize,
|
||||
CancellationToken cancellationToken)
|
||||
where T : class
|
||||
{
|
||||
PreparedCollisionTypeContract.Validate(type);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
Interlocked.Increment(ref _collisionReads);
|
||||
|
||||
ulong key = PakKey.Compose(type, sourceFileId);
|
||||
PakObjectReadStatus status =
|
||||
_reader.ReadBlobBytes(key, out byte[]? bytes);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (status == PakObjectReadStatus.Missing)
|
||||
{
|
||||
Interlocked.Increment(ref _collisionMissing);
|
||||
return PreparedCollisionReadResult<T>.Missing;
|
||||
}
|
||||
if (status == PakObjectReadStatus.Corrupt || bytes is null)
|
||||
{
|
||||
Interlocked.Increment(ref _collisionCorrupt);
|
||||
return PreparedCollisionReadResult<T>.Corrupt;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
T data = deserialize(bytes, cancellationToken);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
Interlocked.Increment(ref _collisionLoaded);
|
||||
return PreparedCollisionReadResult<T>.Loaded(data);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_reader.MarkPayloadCorrupt(
|
||||
key,
|
||||
$"collision deserialization failed despite matching CRC: " +
|
||||
$"{exception.GetType().Name}: {exception.Message}");
|
||||
Interlocked.Increment(ref _collisionCorrupt);
|
||||
return PreparedCollisionReadResult<T>.Corrupt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,10 +20,11 @@ public static class PakFormat {
|
|||
/// Identity of the bake algorithm that produced the payloads. Version 2
|
||||
/// introduced content-identity EnvCell blobs with ordinary TOC aliases;
|
||||
/// version 3 embeds exact render-pass translucency in each texture batch
|
||||
/// so production never rebuilds surface metadata from live DAT.
|
||||
/// so production never rebuilds surface metadata from live DAT. Version 4
|
||||
/// adds complete immutable flat collision and EnvCell-topology payloads.
|
||||
/// The binary format remains version 1.
|
||||
/// </summary>
|
||||
public const uint CurrentBakeToolVersion = 3;
|
||||
public const uint CurrentBakeToolVersion = 4;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ public enum PakAssetType : byte {
|
|||
GfxObjMesh = 1,
|
||||
SetupMesh = 2,
|
||||
EnvCellMesh = 3,
|
||||
GfxObjCollision = 4,
|
||||
SetupCollision = 5,
|
||||
CellStructureCollision = 6,
|
||||
EnvCellTopology = 7,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -145,6 +145,44 @@ public sealed class PakReader : IDisposable {
|
|||
ulong key,
|
||||
out ObjectMeshData? data) {
|
||||
data = null;
|
||||
PakAssetType type = PakKey.Decompose(key).Type;
|
||||
if (type is not (
|
||||
PakAssetType.GfxObjMesh or
|
||||
PakAssetType.SetupMesh or
|
||||
PakAssetType.EnvCellMesh)) {
|
||||
return PakObjectReadStatus.Missing;
|
||||
}
|
||||
|
||||
PakObjectReadStatus status = ReadBlobBytes(key, out byte[]? bytes);
|
||||
if (status != PakObjectReadStatus.Loaded || bytes is null)
|
||||
return status;
|
||||
|
||||
try {
|
||||
data = ObjectMeshDataSerializer.Read(bytes);
|
||||
return PakObjectReadStatus.Loaded;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// Structurally malformed blob behind a valid CRC (bake-side bug or
|
||||
// a tamper that recomputed the CRC). External-file input: demote
|
||||
// to missing, log once — never propagate from a lookup.
|
||||
data = null;
|
||||
MarkPayloadCorrupt(
|
||||
key,
|
||||
$"deserialization failed despite matching CRC: " +
|
||||
$"{ex.GetType().Name}: {ex.Message}");
|
||||
return PakObjectReadStatus.Corrupt;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies and CRC-verifies one typed blob without interpreting its schema.
|
||||
/// Content-layer typed sources deserialize the returned bytes and call
|
||||
/// <see cref="MarkPayloadCorrupt"/> on structural failure.
|
||||
/// </summary>
|
||||
internal PakObjectReadStatus ReadBlobBytes(
|
||||
ulong key,
|
||||
out byte[]? bytes) {
|
||||
bytes = null;
|
||||
int index = BinarySearch(key);
|
||||
if (index < 0) return PakObjectReadStatus.Missing;
|
||||
|
||||
|
|
@ -156,7 +194,7 @@ public sealed class PakReader : IDisposable {
|
|||
// Single pass (review finding 4): ONE copy out of the map; CRC and
|
||||
// deserialization both run over this same buffer.
|
||||
ref readonly var entry = ref _toc[index];
|
||||
var bytes = new byte[entry.Length];
|
||||
bytes = new byte[entry.Length];
|
||||
_accessor.ReadArray((long)entry.Offset, bytes, 0, (int)entry.Length);
|
||||
|
||||
if (!judged) {
|
||||
|
|
@ -164,24 +202,21 @@ public sealed class PakReader : IDisposable {
|
|||
if (actualCrc != entry.Crc32) {
|
||||
_entryVerdictByTocIndex[index] = 0;
|
||||
LogCorruptionOnce(index, $"crc mismatch (expected 0x{entry.Crc32:X8}, got 0x{actualCrc:X8})");
|
||||
bytes = null;
|
||||
return PakObjectReadStatus.Corrupt;
|
||||
}
|
||||
_entryVerdictByTocIndex[index] = 1;
|
||||
}
|
||||
|
||||
try {
|
||||
data = ObjectMeshDataSerializer.Read(bytes);
|
||||
return PakObjectReadStatus.Loaded;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// Structurally malformed blob behind a valid CRC (bake-side bug or
|
||||
// a tamper that recomputed the CRC). External-file input: demote
|
||||
// to missing, log once — never propagate from a lookup.
|
||||
data = null;
|
||||
_entryVerdictByTocIndex[index] = 0;
|
||||
LogCorruptionOnce(index, $"deserialization failed despite matching CRC: {ex.GetType().Name}: {ex.Message}");
|
||||
return PakObjectReadStatus.Corrupt;
|
||||
}
|
||||
return PakObjectReadStatus.Loaded;
|
||||
}
|
||||
|
||||
internal void MarkPayloadCorrupt(ulong key, string reason) {
|
||||
int index = BinarySearch(key);
|
||||
if (index < 0)
|
||||
return;
|
||||
_entryVerdictByTocIndex[index] = 0;
|
||||
LogCorruptionOnce(index, reason);
|
||||
}
|
||||
|
||||
/// <summary>Returns the blob's file offset for alignment assertions in tests.</summary>
|
||||
|
|
@ -198,6 +233,20 @@ public sealed class PakReader : IDisposable {
|
|||
return _toc[index];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Counts one typed catalog partition without touching payload bytes.
|
||||
/// Used by the offline publisher to prove package completeness.
|
||||
/// </summary>
|
||||
public int CountEntries(PakAssetType type) {
|
||||
byte rawType = (byte)type;
|
||||
int count = 0;
|
||||
for (int i = 0; i < _toc.Length; i++) {
|
||||
if ((byte)(_toc[i].Key >> 56) == rawType)
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the complete immutable TOC before an offline bake publishes
|
||||
/// it. This is intentionally stricter than runtime lookup: key order and
|
||||
|
|
|
|||
|
|
@ -46,28 +46,51 @@ public sealed class PakWriter : IDisposable {
|
|||
/// </summary>
|
||||
public void AddBlob(ulong key, ObjectMeshData data) {
|
||||
ThrowIfFinished();
|
||||
if (!_seenKeys.Add(key)) {
|
||||
throw new ArgumentException($"duplicate pak key 0x{key:X16}", nameof(key));
|
||||
}
|
||||
ReserveKey(key);
|
||||
|
||||
long offset = _stream.Position;
|
||||
using var ms = new MemoryStream();
|
||||
ObjectMeshDataSerializer.Write(data, ms);
|
||||
var bytes = ms.ToArray();
|
||||
AddReservedBlob(key, bytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an already serialized immutable payload. This is the package seam
|
||||
/// used by typed non-render assets such as flat collision records.
|
||||
/// </summary>
|
||||
public void AddBlob(ulong key, ReadOnlySpan<byte> bytes) {
|
||||
ThrowIfFinished();
|
||||
ReserveKey(key);
|
||||
AddReservedBlob(key, bytes);
|
||||
}
|
||||
|
||||
private void AddReservedBlob(ulong key, ReadOnlySpan<byte> bytes) {
|
||||
if ((ulong)bytes.Length > uint.MaxValue) {
|
||||
throw new ArgumentException(
|
||||
"one pak payload cannot exceed UInt32.MaxValue bytes",
|
||||
nameof(bytes));
|
||||
}
|
||||
|
||||
long offset = _stream.Position;
|
||||
_stream.Write(bytes);
|
||||
PadToAlignment();
|
||||
|
||||
var receipt = new PakTocEntry {
|
||||
Key = key,
|
||||
Offset = (ulong)offset,
|
||||
Length = (uint)bytes.Length,
|
||||
Length = checked((uint)bytes.Length),
|
||||
Crc32 = Content.Pak.Crc32.Compute(bytes),
|
||||
};
|
||||
_tocEntries.Add(receipt);
|
||||
_receiptByKey.Add(key, receipt);
|
||||
}
|
||||
|
||||
private void ReserveKey(ulong key) {
|
||||
if (!_seenKeys.Add(key)) {
|
||||
throw new ArgumentException($"duplicate pak key 0x{key:X16}", nameof(key));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a second independently addressable key for an existing physical
|
||||
/// blob. The alias receives a normal TOC row with the source blob's exact
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
using System.Collections.Immutable;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Enums;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.Core.Physics;
|
||||
|
|
@ -15,6 +17,13 @@ namespace AcDream.Core.Physics;
|
|||
/// </summary>
|
||||
public static class FlatCollisionAssetBuilder
|
||||
{
|
||||
private static FlatPhysicsBsp EmptyPhysicsBsp() =>
|
||||
new(
|
||||
-1,
|
||||
ImmutableArray<FlatPhysicsBspNode>.Empty,
|
||||
ImmutableArray<int>.Empty,
|
||||
FlatPolygonTable.Empty);
|
||||
|
||||
public static FlatPhysicsBsp FlattenPhysicsBsp(
|
||||
PhysicsBSPNode? root,
|
||||
IReadOnlyDictionary<ushort, ResolvedPolygon> resolved)
|
||||
|
|
@ -270,6 +279,53 @@ public static class FlatCollisionAssetBuilder
|
|||
source.StepDownHeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares the exact Setup collision fields used by
|
||||
/// <see cref="PhysicsDataCache.CacheSetup"/> without retaining the parsed
|
||||
/// DAT object. Source list order and every float bit are preserved.
|
||||
/// </summary>
|
||||
public static FlatSetupCollision FlattenSetup(Setup source)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
|
||||
var cylinders = ImmutableArray.CreateBuilder<FlatCollisionCylinder>(
|
||||
source.CylSpheres?.Count ?? 0);
|
||||
if (source.CylSpheres is not null)
|
||||
{
|
||||
foreach (CylSphere cylinder in source.CylSpheres)
|
||||
{
|
||||
if (cylinder is null)
|
||||
throw new InvalidDataException("Setup cylinder row is null.");
|
||||
cylinders.Add(new FlatCollisionCylinder(
|
||||
cylinder.Origin,
|
||||
cylinder.Radius,
|
||||
cylinder.Height));
|
||||
}
|
||||
}
|
||||
|
||||
var spheres = ImmutableArray.CreateBuilder<FlatCollisionSphere>(
|
||||
source.Spheres?.Count ?? 0);
|
||||
if (source.Spheres is not null)
|
||||
{
|
||||
foreach (Sphere sphere in source.Spheres)
|
||||
{
|
||||
if (sphere is null)
|
||||
throw new InvalidDataException("Setup sphere row is null.");
|
||||
spheres.Add(new FlatCollisionSphere(
|
||||
sphere.Origin,
|
||||
sphere.Radius));
|
||||
}
|
||||
}
|
||||
|
||||
return new FlatSetupCollision(
|
||||
cylinders.MoveToImmutable(),
|
||||
spheres.MoveToImmutable(),
|
||||
source.Height,
|
||||
source.Radius,
|
||||
source.StepUpHeight,
|
||||
source.StepDownHeight);
|
||||
}
|
||||
|
||||
public static FlatGfxObjCollisionAsset FlattenGfxObj(
|
||||
GfxObjPhysics source,
|
||||
GfxObjVisualBounds? visualBounds = null)
|
||||
|
|
@ -297,6 +353,139 @@ public static class FlatCollisionAssetBuilder
|
|||
flatVisualBounds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares one parsed GfxObj and drops the source graph. Objects without
|
||||
/// a retail physics BSP still retain their visual bounds because the
|
||||
/// current runtime uses those bounds as its collision fallback.
|
||||
/// </summary>
|
||||
public static FlatGfxObjCollisionAsset FlattenGfxObj(GfxObj source)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
|
||||
GfxObjVisualBounds? visualBounds = source.VertexArray is null
|
||||
? null
|
||||
: PhysicsDataCache.ComputeVisualBounds(source.VertexArray);
|
||||
FlatGfxObjVisualBounds? flatVisualBounds = visualBounds is null
|
||||
? null
|
||||
: new FlatGfxObjVisualBounds(
|
||||
visualBounds.Min,
|
||||
visualBounds.Max,
|
||||
visualBounds.Center,
|
||||
visualBounds.Radius,
|
||||
visualBounds.HalfExtents);
|
||||
|
||||
bool hasPhysics =
|
||||
source.Flags.HasFlag(GfxObjFlags.HasPhysics)
|
||||
&& source.PhysicsBSP?.Root is not null
|
||||
&& source.VertexArray is not null;
|
||||
if (!hasPhysics)
|
||||
{
|
||||
return new FlatGfxObjCollisionAsset(
|
||||
EmptyPhysicsBsp(),
|
||||
null,
|
||||
flatVisualBounds);
|
||||
}
|
||||
|
||||
Dictionary<ushort, ResolvedPolygon> resolved =
|
||||
PhysicsDataCache.ResolvePolygons(
|
||||
source.PhysicsPolygons,
|
||||
source.VertexArray!);
|
||||
Sphere boundingSphere = source.PhysicsBSP!.Root!.BoundingSphere
|
||||
?? throw new InvalidDataException(
|
||||
"GfxObj physics BSP root has no bounding sphere.");
|
||||
|
||||
return new FlatGfxObjCollisionAsset(
|
||||
FlattenPhysicsBsp(source.PhysicsBSP.Root, resolved),
|
||||
new FlatCollisionSphere(
|
||||
boundingSphere.Origin,
|
||||
boundingSphere.Radius),
|
||||
flatVisualBounds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares reusable CellStruct collision geometry independently of any
|
||||
/// EnvCell placement or topology.
|
||||
/// </summary>
|
||||
public static FlatCellStructureCollisionAsset FlattenCellStructure(
|
||||
CellStruct source)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
|
||||
FlatPhysicsBsp physicsBsp;
|
||||
if (source.PhysicsBSP?.Root is null)
|
||||
{
|
||||
physicsBsp = EmptyPhysicsBsp();
|
||||
}
|
||||
else
|
||||
{
|
||||
Dictionary<ushort, ResolvedPolygon> resolved =
|
||||
PhysicsDataCache.ResolvePolygons(
|
||||
source.PhysicsPolygons,
|
||||
source.VertexArray);
|
||||
physicsBsp = FlattenPhysicsBsp(
|
||||
source.PhysicsBSP.Root,
|
||||
resolved);
|
||||
}
|
||||
|
||||
Dictionary<ushort, ResolvedPolygon> portalResolved =
|
||||
PhysicsDataCache.ResolvePolygons(
|
||||
source.Polygons,
|
||||
source.VertexArray);
|
||||
return new FlatCellStructureCollisionAsset(
|
||||
physicsBsp,
|
||||
FlattenCellContainmentBsp(source.CellBSP?.Root),
|
||||
FlattenPolygonTable(portalResolved));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares the per-cell portion of collision publication. Other-cell IDs
|
||||
/// are expanded with the owning landblock exactly as
|
||||
/// <see cref="PhysicsDataCache.CacheCellStruct"/> does.
|
||||
/// </summary>
|
||||
public static FlatEnvCellTopology FlattenEnvCellTopology(
|
||||
uint envCellId,
|
||||
EnvCell source,
|
||||
FlatPolygonTable portalPolygons)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
ArgumentNullException.ThrowIfNull(portalPolygons);
|
||||
|
||||
var portals = ImmutableArray.CreateBuilder<FlatEnvCellPortal>(
|
||||
source.CellPortals.Count);
|
||||
foreach (CellPortal portal in source.CellPortals)
|
||||
{
|
||||
ushort polygonId = portal.PolygonId;
|
||||
if (!portalPolygons.TryFindPolygonIndex(
|
||||
polygonId,
|
||||
out int polygonIndex))
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"EnvCell 0x{envCellId:X8} portal references missing " +
|
||||
$"polygon 0x{polygonId:X4}.");
|
||||
}
|
||||
|
||||
portals.Add(new FlatEnvCellPortal(
|
||||
portal.OtherCellId,
|
||||
polygonId,
|
||||
(ushort)portal.Flags,
|
||||
polygonIndex));
|
||||
}
|
||||
|
||||
uint prefix = envCellId & 0xFFFF_0000u;
|
||||
uint[] visibleIds = source.VisibleCells is null
|
||||
? []
|
||||
: source.VisibleCells
|
||||
.Select(id => prefix | id)
|
||||
.Distinct()
|
||||
.Order()
|
||||
.ToArray();
|
||||
|
||||
return new FlatEnvCellTopology(
|
||||
portals.MoveToImmutable(),
|
||||
ImmutableArray.Create(visibleIds),
|
||||
source.Flags.HasFlag(EnvCellFlags.SeenOutside));
|
||||
}
|
||||
|
||||
public static FlatCellCollisionAsset FlattenCell(CellPhysics source)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
|
|
@ -310,9 +499,31 @@ public static class FlatCollisionAssetBuilder
|
|||
source.PortalPolygons
|
||||
?? new Dictionary<ushort, ResolvedPolygon>());
|
||||
|
||||
var portals = ImmutableArray.CreateBuilder<FlatEnvCellPortal>(
|
||||
source.Portals.Count);
|
||||
foreach (PortalInfo portal in source.Portals)
|
||||
FlatEnvCellTopology topology = FlattenEnvCellTopology(
|
||||
source.Portals,
|
||||
source.VisibleCellIds,
|
||||
source.SeenOutside,
|
||||
portalPolygons);
|
||||
var structure = new FlatCellStructureCollisionAsset(
|
||||
physicsBsp,
|
||||
containmentBsp,
|
||||
portalPolygons);
|
||||
return new FlatCellCollisionAsset(structure, topology);
|
||||
}
|
||||
|
||||
public static FlatEnvCellTopology FlattenEnvCellTopology(
|
||||
IReadOnlyList<PortalInfo> portals,
|
||||
IEnumerable<uint> visibleCellIds,
|
||||
bool seenOutside,
|
||||
FlatPolygonTable portalPolygons)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(portals);
|
||||
ArgumentNullException.ThrowIfNull(visibleCellIds);
|
||||
ArgumentNullException.ThrowIfNull(portalPolygons);
|
||||
|
||||
var flatPortals = ImmutableArray.CreateBuilder<FlatEnvCellPortal>(
|
||||
portals.Count);
|
||||
foreach (PortalInfo portal in portals)
|
||||
{
|
||||
if (!portalPolygons.TryFindPolygonIndex(
|
||||
portal.PolygonId,
|
||||
|
|
@ -323,24 +534,21 @@ public static class FlatCollisionAssetBuilder
|
|||
$"0x{portal.PolygonId:X4}.");
|
||||
}
|
||||
|
||||
portals.Add(new FlatEnvCellPortal(
|
||||
flatPortals.Add(new FlatEnvCellPortal(
|
||||
portal.OtherCellId,
|
||||
portal.PolygonId,
|
||||
portal.Flags,
|
||||
polygonIndex));
|
||||
}
|
||||
|
||||
uint[] visibleIds = source.VisibleCellIds.ToArray();
|
||||
Array.Sort(visibleIds);
|
||||
var topology = new FlatEnvCellTopology(
|
||||
portals.MoveToImmutable(),
|
||||
uint[] visibleIds = visibleCellIds
|
||||
.Distinct()
|
||||
.Order()
|
||||
.ToArray();
|
||||
return new FlatEnvCellTopology(
|
||||
flatPortals.MoveToImmutable(),
|
||||
ImmutableArray.Create(visibleIds),
|
||||
source.SeenOutside);
|
||||
var structure = new FlatCellStructureCollisionAsset(
|
||||
physicsBsp,
|
||||
containmentBsp,
|
||||
portalPolygons);
|
||||
return new FlatCellCollisionAsset(structure, topology);
|
||||
seenOutside);
|
||||
}
|
||||
|
||||
private static void AttachChild(
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ public sealed class PhysicsDataCache
|
|||
/// Used as a fallback collision shape for entities whose Setup has no
|
||||
/// physics data — we approximate collision using the visual extent.
|
||||
/// </summary>
|
||||
private static GfxObjVisualBounds ComputeVisualBounds(VertexArray vertexArray)
|
||||
internal static GfxObjVisualBounds ComputeVisualBounds(VertexArray vertexArray)
|
||||
{
|
||||
if (vertexArray.Vertices == null || vertexArray.Vertices.Count == 0)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue