using AcDream.Core.Rendering.Wb; namespace AcDream.Bake; /// /// The identity-bearing portion of one EnvCell DAT record needed by the bake. /// Position, portals, and static objects are instance data and intentionally do /// not participate in shared shell geometry. /// public sealed record EnvCellBakeSource( uint FileId, uint EnvironmentId, ushort CellStructure, IReadOnlyList Surfaces); /// One unique shell geometry and every independently addressable cell key that uses it. public sealed record EnvCellGeometryGroup( ulong GeometryId, uint EnvironmentId, ushort CellStructure, ushort[] Surfaces, uint[] FileIds); /// /// Builds the deterministic file-id alias map used by the offline bake. /// Geometry equality is checked against the complete source tuple so a hash /// collision can never silently merge different cells. /// public sealed class EnvCellBakeCatalog { public IReadOnlyList Groups { get; } public int CellCount { get; } public int UniqueGeometryCount => Groups.Count; public int AliasCount => CellCount - UniqueGeometryCount; private EnvCellBakeCatalog(IReadOnlyList groups, int cellCount) { Groups = groups; CellCount = cellCount; } public static EnvCellBakeCatalog Build(IEnumerable sources) => Build(sources, EnvCellGeometryIdentity.Compute); /// /// Identity injection exists for collision-path conformance tests. Production /// always calls the overload bound to . /// public static EnvCellBakeCatalog Build( IEnumerable sources, Func, ulong> computeIdentity) { ArgumentNullException.ThrowIfNull(sources); ArgumentNullException.ThrowIfNull(computeIdentity); var builder = new EnvCellBakeCatalogBuilder(computeIdentity); foreach (var source in sources) builder.Add(source); return builder.Build(); } internal static EnvCellBakeCatalog Create( IReadOnlyList groups, int cellCount) => new(groups, cellCount); } /// /// Incremental catalog builder used by the full bake so 729,888 EnvCell /// records and their surface lists are never retained at once. /// public sealed class EnvCellBakeCatalogBuilder { private readonly Func, ulong> _computeIdentity; private readonly Dictionary _byGeometry = new(); private readonly HashSet _fileIds = new(); private int _cellCount; private bool _built; public EnvCellBakeCatalogBuilder() : this(EnvCellGeometryIdentity.Compute) { } public EnvCellBakeCatalogBuilder( Func, ulong> computeIdentity) { ArgumentNullException.ThrowIfNull(computeIdentity); _computeIdentity = computeIdentity; } public void Add(EnvCellBakeSource source) { if (_built) throw new InvalidOperationException("the EnvCell bake catalog is already complete"); if (!_fileIds.Add(source.FileId)) throw new InvalidDataException($"duplicate EnvCell file id 0x{source.FileId:X8}"); ulong geometryId = _computeIdentity( source.EnvironmentId, source.CellStructure, source.Surfaces); if (_byGeometry.TryGetValue(geometryId, out var existing)) { if (existing.EnvironmentId != source.EnvironmentId || existing.CellStructure != source.CellStructure || !SurfacesEqual(existing.Surfaces, source.Surfaces)) { throw new InvalidDataException( $"EnvCell geometry identity collision 0x{geometryId:X16}: " + $"cell 0x{existing.FileIds[0]:X8} and cell 0x{source.FileId:X8} " + "have different environment/cell-structure/surface tuples"); } existing.FileIds.Add(source.FileId); } else { _byGeometry.Add( geometryId, new MutableGroup( geometryId, source.EnvironmentId, source.CellStructure, source.Surfaces.ToArray(), new List { source.FileId })); } _cellCount++; } public EnvCellBakeCatalog Build() { if (_built) throw new InvalidOperationException("the EnvCell bake catalog is already complete"); _built = true; foreach (var group in _byGeometry.Values) group.FileIds.Sort(); var groups = _byGeometry.Values .Select(group => new EnvCellGeometryGroup( group.GeometryId, group.EnvironmentId, group.CellStructure, group.Surfaces, group.FileIds.ToArray())) .OrderBy(group => group.FileIds[0]) .ToArray(); return EnvCellBakeCatalog.Create(groups, _cellCount); } private static bool SurfacesEqual( ReadOnlySpan expected, IReadOnlyList actual) { if (expected.Length != actual.Count) return false; for (int i = 0; i < expected.Length; i++) { if (expected[i] != actual[i]) return false; } return true; } private sealed record MutableGroup( ulong GeometryId, uint EnvironmentId, ushort CellStructure, ushort[] Surfaces, List FileIds); }