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
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue