From 66825e0bb8456748c688e9c83c43c8c2cd39bd73 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 5 Sep 2026 20:29:32 +0200 Subject: [PATCH] test(overhaul): integrate guarded cathedral geometry witness --- ...a-geometry-witness-integration-contract.md | 43 + ...lockGeometryMembershipInstalledDatTests.cs | 1244 +++++++++++++++++ 2 files changed, 1287 insertions(+) create mode 100644 tests/AcDream.App.Tests/Streaming/LandblockGeometryMembershipInstalledDatTests.cs diff --git a/docs/research/2026-09-01-overhaul/c1a-geometry-witness-integration-contract.md b/docs/research/2026-09-01-overhaul/c1a-geometry-witness-integration-contract.md index 38f63a09..06f785e3 100644 --- a/docs/research/2026-09-01-overhaul/c1a-geometry-witness-integration-contract.md +++ b/docs/research/2026-09-01-overhaul/c1a-geometry-witness-integration-contract.md @@ -51,3 +51,46 @@ The fresh initialized0..6 part-order assertion is a production initialization witness, not a claim of universal equality with later live-retail retained part-list order0,1,6,5,2,4,3. AD117's source approximation and AP159's boundary ties remain explicit. C1a/G4 unpassed, FPS deferred, known retail leak unchanged. + +## Write-only return — 2026-09-05 + +The test is code-ready in the campaign tree. Lead independently compared it +against the preserved scratch source: exactly eight added lines at actual +ShellCellId mesh extraction, no other change. Campaign source SHA256 +4CA55ED6033D99447E367577EFAE307E34516BDF75245A5C591B007D1F5DAC0B; +original scratch SHA9894EC50BF40F732237E3249E5F7815FDC931DDC66F1D5D212D0CA717373A9C5 +unchanged. Diff-check clean. No AuthoredDiagnostic copied; placeholder and +three-cell/21-entry assertions remain. This is an implementation return, +not independent review approval or execution evidence. No builds/tests or +clients were run under this write-only release. Bridge implementer retains +exclusive execution ownership until its frozen return; the later execution +boundary above is still closed. + +## Lead capture release — after bridge1e645ee40, 2026-09-05 + +Bridge implementation is committed1e645ee40 with its narrowed register row, +managed integrity adaptation and architecture/ledger. Independent bounded +review PASS; exact restored affected52/52 and source hashes verified by lead. +Implementer returned execution ownership; no live build/test handle remains. + +Lead may now commit this guarded original witness and run one Release App +test-project build, then exactly +`CathedralFactoryGeometryAndRetailMembership_AreCompleteAndDeterministic` +once with ACDREAM_RUN_INSTALLED_DAT_TESTS=1 and explicit +`C:/Users/erikn/Documents/Asheron's Call`. Recheck both clients/debugger +absent and ACE UDP9000 first; no agent owns concurrent execution. Preserve +build/stdout/TRX in new `artifacts/s5-c1a-integrated-capture-20260905-r1/`. +No original or authored diagnostic is included in that exact filter. + +The expected first failure is ONLY the unpinned combined-hash assertion, +after all real semantic facts and both fresh owner products agree. Inspect +the exact failure location, all four DAT hashes, three product hashes/counts +and raw byte-equality assertions before accepting that result. A different +failure is a bounded finding; do not change the three-cell/21-entry oracle, +production or any predicate to make this test pass. No new numerical retry +or authored-diagnostic fix belongs to this release. + +Pinning the observed combined hash and the later product mutations require +their recorded lead release after this capture. Full canonical/hermetic, +two clean-checkpoint focused processes and final graphical gates remain owed. +No C1a/G4 pass, FPS work or known retail leak change is authorized here. diff --git a/tests/AcDream.App.Tests/Streaming/LandblockGeometryMembershipInstalledDatTests.cs b/tests/AcDream.App.Tests/Streaming/LandblockGeometryMembershipInstalledDatTests.cs new file mode 100644 index 00000000..df0ccdbc --- /dev/null +++ b/tests/AcDream.App.Tests/Streaming/LandblockGeometryMembershipInstalledDatTests.cs @@ -0,0 +1,1244 @@ +using System.Collections.Immutable; +using System.Numerics; +using System.Reflection; +using System.Security.Cryptography; +using AcDream.App.Rendering; +using AcDream.App.Rendering.Walk; +using AcDream.App.Rendering.Wb; +using AcDream.App.Streaming; +using AcDream.Content; +using AcDream.Content.Pak; +using AcDream.Core.Meshing; +using AcDream.Core.Physics; +using AcDream.Core.World; +using Chorizite.Core.Render.Enums; +using DatReaderWriter.DBObjs; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit.Abstractions; +using DatEnvironment = DatReaderWriter.DBObjs.Environment; +using DatEnvCell = DatReaderWriter.DBObjs.EnvCell; +using DatGfxObj = DatReaderWriter.DBObjs.GfxObj; +using DatSetup = DatReaderWriter.DBObjs.Setup; + +namespace AcDream.App.Tests.Streaming; + +/// +/// C1a's installed-retail-DAT witness for the complete F418 geometry and +/// render-membership product. This deliberately consumes the real worker +/// factory, live-DAT extractor, flat collision builder, publication helpers, +/// production cache, and production shadow registry. It contains no second +/// membership implementation or synthetic expected entry list. +/// +[Trait("Lane", "InstalledDat")] +public sealed partial class LandblockGeometryMembershipInstalledDatTests +{ + private const uint LandblockId = 0xF418FFFFu; + private const uint ShellCellId = 0xF4180104u; + private const uint RampSetupId = 0x020009A2u; + private const string ExpectedCombinedHash = "REPLACE_AFTER_FIRST_VERIFIED_CAPTURE"; + + private static readonly (string Name, string Sha256)[] AcceptedDatFiles = + [ + ("client_portal.dat", "DC6E500BA22E6B186DB7171E3F3345238B6444C85D798ADC85E550973B8D12E4"), + ("client_cell_1.dat", "6DB0ABF00FBCEED62C3F1EE842EE7C1F423D732BED77A5B7C102EE89A52AB99E"), + ("client_highres.dat", "503E0828D14F2F9CCBC31431E1055AC188464BF4B499DE37F4C3D5B2D9F3E727"), + ("client_local_English.dat", "E85C820280C88FAC7DF6C8043F5E24596E9C8774193AF4123D756546F78FB2BB"), + ]; + + private readonly ITestOutputHelper _output; + + public LandblockGeometryMembershipInstalledDatTests(ITestOutputHelper output) => + _output = output; + + [Fact] + public void CathedralFactoryGeometryAndRetailMembership_AreCompleteAndDeterministic() + { + string datDirectory = RequireExplicitInstalledDatDirectory(); + VerifyInstalledFileIdentity(datDirectory); + + Capture first = CaptureProduct(datDirectory); + Capture second = CaptureProduct(datDirectory); + + _output.WriteLine($"geometry-sha256={first.GeometryHash}"); + _output.WriteLine($"membership-sha256={first.MembershipHash}"); + _output.WriteLine($"combined-sha256={first.CombinedHash}"); + _output.WriteLine($"counts={first.Counts}"); + + Assert.Equal(first.GeometryHash, second.GeometryHash); + Assert.Equal(first.MembershipHash, second.MembershipHash); + Assert.Equal(first.CombinedHash, second.CombinedHash); + Assert.Equal(first.Counts, second.Counts); + Assert.Equal(first.GeometryBytes, second.GeometryBytes); + Assert.Equal(first.MembershipBytes, second.MembershipBytes); + Assert.Equal(ExpectedCombinedHash, first.CombinedHash); + } + + [Fact] + [Trait("Purpose", "Diagnostic")] + public void CathedralRampBuildingBridge_ReportsFiniteRetailBoxPredicates() + { + string datDirectory = RequireExplicitInstalledDatDirectory(); + VerifyInstalledFileIdentity(datDirectory); + + _ = WithPublishedProduct( + datDirectory, + (build, _, cache, engine, _) => + { + ReportRampBuildingBridge(build, cache, engine); + return true; + }); + } + + private void VerifyInstalledFileIdentity(string datDirectory) + { + foreach ((string name, string accepted) in AcceptedDatFiles) + { + string path = Path.Combine(datDirectory, name); + Assert.True(File.Exists(path), $"Installed DAT is missing: '{path}'."); + using FileStream stream = File.OpenRead(path); + string actual = Convert.ToHexString(SHA256.HashData(stream)); + _output.WriteLine($"dat-sha256 {name}={actual}"); + Assert.Equal(accepted, actual); + } + } + + private static string RequireExplicitInstalledDatDirectory() + { + Assert.Equal( + "1", + System.Environment.GetEnvironmentVariable("ACDREAM_RUN_INSTALLED_DAT_TESTS")); + string? directory = System.Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR"); + Assert.False( + string.IsNullOrWhiteSpace(directory), + "C1a requires an explicit ACDREAM_DAT_DIR for its installed-DAT identity gate."); + Assert.True(Directory.Exists(directory), $"Installed DAT directory is missing: '{directory}'."); + return directory!; + } + + private static Capture CaptureProduct(string datDirectory) => + WithPublishedProduct( + datDirectory, + static (build, envBuild, _, engine, meshSource) => + { + ProductFacts facts = AssertSemanticFacts(build, envBuild, engine); + (byte[] geometry, GeometryCounts geometryCounts) = + SerializeGeometry(build, envBuild, meshSource); + (byte[] membership, MembershipCounts membershipCounts) = + SerializeMembership(build, envBuild, engine, facts); + byte[] combined = SerializeCombined(geometry, membership); + + return new Capture( + geometry, + membership, + Hash(geometry), + Hash(membership), + Hash(combined), + new ProductCounts(geometryCounts, membershipCounts)); + }); + + private static T WithPublishedProduct( + string datDirectory, + Func consume) + { + using var dat = new BoundedTestDatCollection(datDirectory); + var bounded = (IDatReaderWriter)dat; + Region region = Assert.IsType(bounded.Get(0x13000000u)); + float[] heights = region.LandDefs.LandHeightTable; + Assert.True(heights.Length >= 256); + + using var collisionSource = new InstalledDatPreparedCollisionAdapter(bounded); + using var meshSource = new DatPreparedAssetSource(bounded, NullLogger.Instance); + var factory = new LandblockBuildFactory( + bounded, + collisionSource, + new object(), + heights); + var request = new LandblockBuildRequest( + LandblockId, + LandblockStreamJobKind.LoadNear, + Generation: 1, + new LandblockBuildOrigin(0xF4, 0x18)); + + LandblockBuild build = Assert.IsType(factory.Build(request)); + EnvCellLandblockBuild envBuild = Assert.IsType(build.EnvCells); + LandblockCollisionBuild collisions = Assert.IsType(build.Collisions); + Assert.Equal(Vector3.Zero, PublicationOrigin(build.Origin)); + + var cache = PhysicsDataCache.CreateProduction(); + var engine = new PhysicsEngine { DataCache = cache }; + Vector3 origin = PublicationOrigin(build.Origin); + TerrainSurface terrain = LandblockPhysicsContentBuilder.BuildTerrainSurface( + build.Landblock, + heights); + var cellSurfaces = new List(); + var portalPlanes = new List(); + LandblockPhysicsContentBuilder.PublishPreparedCells( + cache, + build.Landblock, + collisions, + origin, + cellSurfaces, + portalPlanes); + LandblockPhysicsContentBuilder.CacheBuildings( + cache, + build.Landblock, + terrain, + origin); + LandblockPhysicsContentBuilder.CachePreparedObjects(cache, collisions); + engine.AddLandblock( + build.LandblockId, + terrain, + cellSurfaces, + portalPlanes, + origin.X, + origin.Y); + _ = LandblockPhysicsContentBuilder.PublishStaticCollision( + engine, + cache, + build.Landblock, + collisions, + origin); + + return consume(build, envBuild, cache, engine, meshSource); + } + + private void ReportRampBuildingBridge( + LandblockBuild build, + PhysicsDataCache cache, + PhysicsEngine engine) + { + const uint outdoorCellId = 0xF4180009u; + const uint destinationCellId = 0xF4180114u; + const float epsilon = 0.000199999995f; + + WorldEntity ramp = Assert.Single( + build.Landblock.Entities, + entity => !entity.IsBuildingShell + && entity.SourceGfxObjOrSetupId == RampSetupId); + IReadOnlyList retainedParts = + GetRetainedPartArray(engine.ShadowObjects, ramp.Id); + Assert.Equal(ramp.MeshRefs.Count, retainedParts.Count); + + IReadOnlyList partBoxes = InvokePrivateStatic< + IReadOnlyList>( + typeof(ShadowObjectRegistry), + "BuildFloodPartBoxes", + ramp.Position, + ramp.Rotation, + retainedParts); + IReadOnlyList partSpheres = + InvokePrivateStatic>( + typeof(ShadowObjectRegistry), + "BuildBspPartSpheres", + ramp.Position, + ramp.Rotation, + retainedParts); + Assert.Equal(retainedParts.Count, partBoxes.Count); + Assert.Equal(retainedParts.Count, partSpheres.Count); + + BuildingPhysics building = Assert.IsType( + cache.GetBuilding(outdoorCellId)); + CellPhysics destination = Assert.IsType( + cache.GetCellStruct(destinationCellId)); + (int BuildingPortalIndex, BldPortalInfo BuildingPortal)[] bridges = + building.Portals + .Select((portal, index) => (index, portal)) + .Where(candidate => candidate.portal.OtherCellId == destinationCellId) + .ToArray(); + Assert.NotEmpty(bridges); + + foreach ((int BuildingPortalIndex, BldPortalInfo BuildingPortal) bridge in bridges) + { + Assert.True(bridge.BuildingPortal.OtherPortalId >= 0); + int reciprocalIndex = bridge.BuildingPortal.OtherPortalId; + Assert.InRange(reciprocalIndex, 0, destination.Portals.Count - 1); + PortalInfo reciprocal = destination.Portals[reciprocalIndex]; + Plane portalPlane = ResolvePortalPlane( + destination, + reciprocalIndex, + reciprocal); + int rawPortalSide = reciprocal.PortalSide ? 1 : 0; + + _output.WriteLine( + $"bridge owner=0x{ramp.Id:X8} setup=0x{RampSetupId:X8} " + + $"from=0x{outdoorCellId:X8} buildingPortal={bridge.BuildingPortalIndex} " + + $"to=0x{destinationCellId:X8} reciprocal={reciprocalIndex} " + + $"flags=0x{bridge.BuildingPortal.Flags:X4} reciprocalOther=0x{reciprocal.OtherCellId:X4} " + + $"reciprocalFlags=0x{reciprocal.Flags:X4} side={rawPortalSide}"); + _output.WriteLine($"destination-world={FormatMatrix(destination.WorldTransform)}"); + _output.WriteLine($"destination-inverse={FormatMatrix(destination.InverseWorldTransform)}"); + _output.WriteLine( + $"portal-plane n={FormatVector(portalPlane.Normal)} d={FormatFloat(portalPlane.D)}"); + _output.WriteLine( + "shared-primitives box-transform=ShadowPartBox.RefitToLocal " + + "box-containment=FlatBspQuery.BoxIntersectsCellBsp " + + "sphere-containment=FlatBspQuery.SphereIntersectsCellBsp"); + + FlatCellContainmentBsp containment = Assert.IsType( + destination.FlatContainmentBsp); + bool anyRetailBoxAdmit = false; + bool anyCurrentSphereAdmit = false; + for (int partIndex = 0; partIndex < retainedParts.Count; partIndex++) + { + ShadowShape shape = retainedParts[partIndex]; + ShadowPartBox box = partBoxes[partIndex]; + DatReaderWriter.Types.Sphere sphere = partSpheres[partIndex]; + Vector3 localSphereCenter = Vector3.Transform( + sphere.Origin, + destination.InverseWorldTransform); + double spherePlaneDistance = DotWide(portalPlane, localSphereCenter); + float paddedRadius = sphere.Radius + epsilon; + bool cheapRejectPass = rawPortalSide == 1 + ? spherePlaneDistance <= paddedRadius + : spherePlaneDistance >= -paddedRadius; + double cheapTieMargin = rawPortalSide == 1 + ? Math.Abs(spherePlaneDistance - paddedRadius) + : Math.Abs(spherePlaneDistance + paddedRadius); + + box.RefitToLocal( + destination.InverseWorldTransform, + out Vector3 localBoxMin, + out Vector3 localBoxMax); + RetailBoxClassification classification = ClassifyRetailBox( + portalPlane, + localBoxMin, + localBoxMax, + epsilon); + bool planeAdmit = + classification.Side == RetailPlaneSide.Crossing + || (int)classification.Side == rawPortalSide; + bool boxContainment = FlatBspQuery.BoxIntersectsCellBsp( + containment, + localBoxMin, + localBoxMax); + bool sphereContainment = FlatBspQuery.SphereIntersectsCellBsp( + containment, + localSphereCenter, + sphere.Radius); + bool retailBoxAdmit = cheapRejectPass && planeAdmit && boxContainment; + anyRetailBoxAdmit |= retailBoxAdmit; + anyCurrentSphereAdmit |= sphereContainment; + + Assert.True(double.IsFinite(spherePlaneDistance)); + Assert.True(double.IsFinite(cheapTieMargin)); + Assert.NotEqual(0d, cheapTieMargin); + Assert.True(double.IsFinite(classification.EpsilonTieMargin)); + Assert.NotEqual(0d, classification.EpsilonTieMargin); + + _output.WriteLine( + $"buildingPortal={bridge.BuildingPortalIndex} part={partIndex} gfx=0x{shape.GfxObjId:X8} " + + $"sphereWorld={FormatVector(sphere.Origin)} radius={FormatFloat(sphere.Radius)} " + + $"sphereLocal={FormatVector(localSphereCenter)} " + + $"distance={spherePlaneDistance:R} padded={FormatFloat(paddedRadius)} " + + $"cheap={cheapRejectPass} cheapTieMargin={cheapTieMargin:R}"); + _output.WriteLine( + $"buildingPortal={bridge.BuildingPortalIndex} part={partIndex} " + + $"boxLocal={FormatVector(box.LocalMin)}..{FormatVector(box.LocalMax)} " + + $"boxWorldPosition={FormatVector(box.WorldPosition)} " + + $"boxWorldRotation={FormatQuaternion(box.WorldRotation)} " + + $"destBox={FormatVector(localBoxMin)}..{FormatVector(localBoxMax)} " + + $"cornerSides={string.Join(',', classification.CornerSides)} " + + $"boxSide={classification.Side} cornerTieMargin={classification.EpsilonTieMargin:R} " + + $"planeAdmit={planeAdmit} boxContain={boxContainment} " + + $"retailBoxAdmit={retailBoxAdmit} currentSphereContain={sphereContainment}"); + } + + _output.WriteLine( + $"portal-summary buildingPortal={bridge.BuildingPortalIndex} " + + $"retailBoxAny={anyRetailBoxAdmit} " + + $"currentSphereAny={anyCurrentSphereAdmit}"); + } + + _output.WriteLine($"bridge-summary portals={bridges.Length}"); + + Assert.True(engine.ShadowObjects.TryGetRetailCellArray(ramp.Id, out var actualCells)); + _output.WriteLine( + $"current-retained-cellarray={string.Join(',', actualCells.Select(id => $"0x{id:X8}"))}"); + foreach (uint sourceCellId in actualCells + .TakeWhile(id => id != destinationCellId) + .Where(id => (id & 0xFFFFu) >= 0x0100u)) + { + CellPhysics sourceCell = Assert.IsType( + cache.GetCellStruct(sourceCellId)); + var candidates = new CellArray(); + CellTransit.FindTransitCellsBox( + cache, + sourceCell, + sourceCellId, + partBoxes, + partSpheres, + candidates, + out bool exitOutside); + _output.WriteLine( + $"normal-box-source=0x{sourceCellId:X8} " + + $"adds0114={candidates.Contains(destinationCellId)} " + + $"exitOutside={exitOutside} " + + $"candidates={string.Join(',', candidates.OrderedIds.Select(id => $"0x{id:X8}"))}"); + } + } + + private static IReadOnlyList GetRetainedPartArray( + ShadowObjectRegistry registry, + uint entityId) + { + PropertyInfo property = Assert.IsAssignableFrom( + typeof(ShadowObjectRegistry).GetProperty( + "_entityRetailPartArrays", + BindingFlags.Instance | BindingFlags.NonPublic)); + var arrays = Assert.IsType>>( + property.GetValue(registry)); + return Assert.IsAssignableFrom>(arrays[entityId]); + } + + private static T InvokePrivateStatic( + Type owner, + string methodName, + params object?[] arguments) + { + MethodInfo method = Assert.IsAssignableFrom(owner.GetMethod( + methodName, + BindingFlags.Static | BindingFlags.NonPublic)); + return Assert.IsAssignableFrom(method.Invoke(null, arguments)); + } + + private static Plane ResolvePortalPlane( + CellPhysics cell, + int portalIndex, + PortalInfo portal) + { + MethodInfo method = Assert.IsAssignableFrom( + typeof(CellTransit).GetMethod( + "TryGetPortalPlane", + BindingFlags.Static | BindingFlags.NonPublic)); + object?[] arguments = [cell, portalIndex, portal, default(Plane)]; + Assert.True(Assert.IsType(method.Invoke(null, arguments))); + return Assert.IsType(arguments[3]); + } + + private static RetailBoxClassification ClassifyRetailBox( + Plane plane, + Vector3 min, + Vector3 max, + float epsilon) + { + Vector3[] corners = + [ + new(min.X, min.Y, min.Z), + new(max.X, max.Y, max.Z), + new(min.X, min.Y, max.Z), + new(min.X, max.Y, min.Z), + new(max.X, min.Y, min.Z), + new(max.X, min.Y, max.Z), + new(min.X, max.Y, max.Z), + new(max.X, max.Y, min.Z), + ]; + var sides = new RetailPlaneSide[corners.Length]; + double epsilonTieMargin = double.MaxValue; + for (int index = 0; index < corners.Length; index++) + { + double distance = DotWide(plane, corners[index]); + Assert.True(double.IsFinite(distance)); + epsilonTieMargin = Math.Min( + epsilonTieMargin, + Math.Min( + Math.Abs(distance - epsilon), + Math.Abs(distance + epsilon))); + sides[index] = distance > epsilon + ? RetailPlaneSide.Positive + : distance < -epsilon + ? RetailPlaneSide.Negative + : RetailPlaneSide.InPlane; + } + + RetailPlaneSide result = sides[0] == RetailPlaneSide.InPlane + ? RetailPlaneSide.Crossing + : sides.Skip(1).Any(side => side != sides[0]) + ? RetailPlaneSide.Crossing + : sides[0]; + return new RetailBoxClassification(result, sides, epsilonTieMargin); + } + + private static double DotWide(Plane plane, Vector3 point) => + (double)plane.Normal.X * point.X + + (double)plane.Normal.Y * point.Y + + (double)plane.Normal.Z * point.Z + + plane.D; + + private static string FormatFloat(float value) => + $"{value:R}/0x{BitConverter.SingleToUInt32Bits(value):X8}"; + + private static string FormatVector(Vector3 value) => + $"({FormatFloat(value.X)},{FormatFloat(value.Y)},{FormatFloat(value.Z)})"; + + private static string FormatQuaternion(Quaternion value) => + $"({FormatFloat(value.X)},{FormatFloat(value.Y)},{FormatFloat(value.Z)},{FormatFloat(value.W)})"; + + private static string FormatMatrix(Matrix4x4 value) => + string.Join(',', new[] + { + value.M11, value.M12, value.M13, value.M14, + value.M21, value.M22, value.M23, value.M24, + value.M31, value.M32, value.M33, value.M34, + value.M41, value.M42, value.M43, value.M44, + }.Select(FormatFloat)); + + private static ProductFacts AssertSemanticFacts( + LandblockBuild build, + EnvCellLandblockBuild envBuild, + PhysicsEngine engine) + { + EnvCellShellPlacement shell = Assert.Single( + envBuild.Shells, + candidate => candidate.CellId == ShellCellId); + Assert.NotEqual(0ul, shell.GeometryId); + + WorldEntity ramp = Assert.Single( + build.Landblock.Entities, + entity => !entity.IsBuildingShell + && entity.SourceGfxObjOrSetupId == RampSetupId); + Assert.Equal(7, ramp.MeshRefs.Count); + Assert.True(engine.ShadowObjects.HasLogicalOwner(ramp.Id)); + Assert.True(engine.ShadowObjects.TryGetRetailCellArray(ramp.Id, out var rampCells)); + Assert.Equal([0xF4180112u, 0xF4180113u, 0xF4180009u], rampCells); + Assert.Equal( + RetailCellArrayRoute.BoundingBox, + engine.ShadowObjects.GetRetailCellArrayRoute(ramp.Id)); + + var rampEntries = new List(); + foreach (uint cellId in rampCells) + { + RetailPartEntry[] entries = engine.ShadowObjects + .GetRetailPartEntriesInCell(cellId) + .Where(entry => entry.EntityId == ramp.Id) + .ToArray(); + Assert.Equal(7, entries.Length); + Assert.Equal(Enumerable.Range(0, 7), entries.Select(entry => entry.PartIndex)); + Assert.All(entries, entry => + { + Assert.Equal(cellId, entry.CellId); + Assert.True(entry.ClipPlanesRequired); + }); + rampEntries.AddRange(entries); + } + Assert.Equal(21, rampEntries.Count); + + HashSet ownerIds = build.Landblock.Entities + .Select(entity => entity.Id) + .ToHashSet(); + var cellDomain = new HashSet( + envBuild.VisibilityCells.Select(cell => cell.CellId)); + foreach (WorldEntity entity in build.Landblock.Entities) + { + if (engine.ShadowObjects.TryGetRetailCellArray(entity.Id, out var cells)) + cellDomain.UnionWith(cells); + } + + uint crossOwnerCell = 0u; + foreach (uint cellId in cellDomain.Order()) + { + IReadOnlyList entries = + engine.ShadowObjects.GetRetailPartEntriesInCell(cellId); + foreach (RetailPartEntry entry in entries) + { + Assert.Contains(entry.EntityId, ownerIds); + Assert.True( + engine.ShadowObjects.TryGetRetailCellArray(entry.EntityId, out var ownerCells)); + Assert.Contains(cellId, ownerCells); + } + if (entries.Select(entry => entry.EntityId).Distinct().Skip(1).Any()) + crossOwnerCell = cellId; + } + Assert.NotEqual(0u, crossOwnerCell); + + Assert.Contains(build.Landblock.Entities, entity => + engine.ShadowObjects.TryGetRetailCellArray(entity.Id, out var cells) + && cells.Count > 1); + Assert.Contains(build.Landblock.Entities, entity => + engine.ShadowObjects.GetRetailCellArrayRoute(entity.Id) + == RetailCellArrayRoute.BoundingBox); + + return new ProductFacts(ramp.Id, shell.GeometryId, crossOwnerCell); + } + + private static (byte[] Bytes, GeometryCounts Counts) SerializeGeometry( + LandblockBuild build, + EnvCellLandblockBuild envBuild, + IPreparedAssetSource meshSource) + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + WriteVersion(writer, "ACDREAM-C1A-GEOMETRY-V1"); + writer.Write(build.LandblockId); + WriteFloat(writer, envBuild.WalkMaxZ); + WriteFloat(writer, envBuild.WalkMinZ); + + int vertexCount = 0; + int subsetCount = 0; + int indexCount = 0; + int setupPartCount = 0; + + writer.Write(envBuild.VisibilityCells.Length); + foreach (LoadedCell cell in envBuild.VisibilityCells) + WriteVisibilityCell(writer, cell); + + writer.Write(envBuild.Shells.Length); + foreach (EnvCellShellPlacement shell in envBuild.Shells) + WriteShellPlacement(writer, shell); + + writer.Write(build.Landblock.Entities.Count); + foreach (WorldEntity entity in build.Landblock.Entities) + WritePublishedEntity(writer, entity); + + writer.Write(envBuild.Shells.Length); + foreach (EnvCellShellPlacement shell in envBuild.Shells) + { + writer.Write(shell.CellId); + writer.Write(shell.GeometryId); + PreparedAssetReadResult read = meshSource.Read( + PreparedAssetRequest.EnvCellGeometry( + shell.CellId, + shell.GeometryId, + shell.EnvironmentId, + shell.CellStructure, + shell.Surfaces)); + ObjectMeshData mesh = RequireMesh(read, "EnvCell", shell.CellId); + if (shell.CellId == ShellCellId) + { + // Establish real shell geometry before the first golden capture. + Assert.NotEmpty(mesh.Vertices); + var shellBatches = ObjectMeshManager.OrderedUploadBatches(mesh); + Assert.NotEmpty(shellBatches); + Assert.True(shellBatches.Sum(subset => subset.Batch.Indices.Count) > 0); + } + WriteMeshProduct( + writer, + mesh, + cellShell: true, + ref vertexCount, + ref subsetCount, + ref indexCount, + ref setupPartCount); + } + + uint[] setupIds = build.Landblock.Entities + .Select(entity => entity.SourceGfxObjOrSetupId) + .Where(id => (id & 0xFF000000u) == 0x02000000u) + .Distinct() + .ToArray(); + writer.Write(setupIds.Length); + foreach (uint setupId in setupIds) + { + writer.Write(setupId); + ObjectMeshData mesh = RequireMesh( + meshSource.Read(PreparedAssetRequest.Setup(setupId)), + "Setup", + setupId); + WriteMeshProduct( + writer, + mesh, + cellShell: false, + ref vertexCount, + ref subsetCount, + ref indexCount, + ref setupPartCount); + } + + uint[] gfxObjIds = build.Landblock.Entities + .SelectMany(entity => entity.MeshRefs) + .Select(mesh => mesh.GfxObjId) + .Distinct() + .ToArray(); + writer.Write(gfxObjIds.Length); + foreach (uint gfxObjId in gfxObjIds) + { + writer.Write(gfxObjId); + ObjectMeshData mesh = RequireMesh( + meshSource.Read(PreparedAssetRequest.GfxObj(gfxObjId)), + "GfxObj", + gfxObjId); + WriteMeshProduct( + writer, + mesh, + cellShell: false, + ref vertexCount, + ref subsetCount, + ref indexCount, + ref setupPartCount); + } + + return ( + stream.ToArray(), + new GeometryCounts( + envBuild.VisibilityCells.Length, + envBuild.Shells.Length, + build.Landblock.Entities.Count, + setupIds.Length, + gfxObjIds.Length, + vertexCount, + subsetCount, + indexCount, + setupPartCount)); + } + + private static (byte[] Bytes, MembershipCounts Counts) SerializeMembership( + LandblockBuild build, + EnvCellLandblockBuild envBuild, + PhysicsEngine engine, + ProductFacts facts) + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + WriteVersion(writer, "ACDREAM-C1A-MEMBERSHIP-V1"); + writer.Write(build.LandblockId); + writer.Write(facts.RampEntityId); + writer.Write(facts.ShellGeometryId); + writer.Write(facts.CrossOwnerCellId); + + int logicalOwnerCount = 0; + int retainedArrayCount = 0; + int retainedCellCount = 0; + var cellDomain = new HashSet( + envBuild.VisibilityCells.Select(cell => cell.CellId)); + + writer.Write(build.Landblock.Entities.Count); + foreach (WorldEntity entity in build.Landblock.Entities) + { + writer.Write(entity.Id); + bool hasLogicalOwner = engine.ShadowObjects.HasLogicalOwner(entity.Id); + writer.Write(hasLogicalOwner); + if (hasLogicalOwner) + logicalOwnerCount++; + + bool hasRetailCellArray = engine.ShadowObjects.TryGetRetailCellArray( + entity.Id, + out IReadOnlyList cells); + writer.Write(hasRetailCellArray); + writer.Write((int)engine.ShadowObjects.GetRetailCellArrayRoute(entity.Id)); + writer.Write(cells.Count); + foreach (uint cellId in cells) + { + writer.Write(cellId); + cellDomain.Add(cellId); + } + if (hasRetailCellArray) + { + retainedArrayCount++; + retainedCellCount += cells.Count; + } + } + + uint[] orderedCellDomain = cellDomain.Order().ToArray(); + int entryCount = 0; + writer.Write(orderedCellDomain.Length); + foreach (uint cellId in orderedCellDomain) + { + writer.Write(cellId); + IReadOnlyList entries = + engine.ShadowObjects.GetRetailPartEntriesInCell(cellId); + writer.Write(entries.Count); + foreach (RetailPartEntry entry in entries) + { + writer.Write(entry.EntityId); + writer.Write(entry.PartIndex); + writer.Write(entry.GfxObjId); + writer.Write(entry.CellId); + writer.Write(entry.ClipPlanesRequired); + entryCount++; + } + } + + return ( + stream.ToArray(), + new MembershipCounts( + build.Landblock.Entities.Count, + logicalOwnerCount, + retainedArrayCount, + retainedCellCount, + orderedCellDomain.Length, + entryCount)); + } + + private static byte[] SerializeCombined(byte[] geometry, byte[] membership) + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + WriteVersion(writer, "ACDREAM-C1A-COMBINED-V1"); + writer.Write(geometry.Length); + writer.Write(geometry); + writer.Write(membership.Length); + writer.Write(membership); + return stream.ToArray(); + } + + private static void WriteVisibilityCell(BinaryWriter writer, LoadedCell cell) + { + writer.Write(cell.CellId); + WriteVector3(writer, cell.WorldPosition); + WriteMatrix(writer, cell.WorldTransform); + WriteMatrix(writer, cell.InverseWorldTransform); + WriteVector3(writer, cell.LocalBoundsMin); + WriteVector3(writer, cell.LocalBoundsMax); + WriteNullableUInt32(writer, cell.BuildingId); + + writer.Write(cell.Portals.Count); + foreach (CellPortalInfo portal in cell.Portals) + { + writer.Write(portal.OtherCellId); + writer.Write(portal.PolygonId); + writer.Write(portal.Flags); + writer.Write(portal.OtherPortalId); + } + writer.Write(cell.ClipPlanes.Count); + foreach (PortalClipPlane plane in cell.ClipPlanes) + { + WriteVector3(writer, plane.Normal); + WriteFloat(writer, plane.D); + writer.Write(plane.InsideSide); + } + writer.Write(cell.PortalPolygons.Count); + foreach (Vector3[] polygon in cell.PortalPolygons) + { + writer.Write(polygon.Length); + foreach (Vector3 vertex in polygon) + WriteVector3(writer, vertex); + } + writer.Write(cell.VisibleCells.Count); + foreach (uint visibleCell in cell.VisibleCells) + writer.Write(visibleCell); + writer.Write(cell.SeenOutside); + writer.Write(cell.IsOutdoorNode); + + writer.Write(cell.Walk is not null); + if (cell.Walk is not { } walk) + return; + writer.Write(walk.CellId); + WriteMatrix(writer, walk.WorldTransform); + WriteMatrix(writer, walk.InverseWorldTransform); + writer.Write(walk.Portals.Length); + foreach (WalkCellPortal portal in walk.Portals) + { + writer.Write(portal.OtherCellId); + writer.Write(portal.PolygonIndex); + writer.Write(portal.PortalSide); + writer.Write(portal.OtherPortalId); + writer.Write(portal.ExactMatch); + } + writer.Write(walk.PortalPolygons.Length); + foreach (WalkPolygon polygon in walk.PortalPolygons) + { + writer.Write(polygon.Vertices.Length); + foreach (Vector3 vertex in polygon.Vertices) + WriteVector3(writer, vertex); + WriteVector3(writer, polygon.Plane.Normal); + WriteFloat(writer, polygon.Plane.D); + } + writer.Write(walk.StabList.Length); + foreach (uint cellId in walk.StabList) + writer.Write(cellId); + } + + private static void WriteShellPlacement(BinaryWriter writer, EnvCellShellPlacement shell) + { + writer.Write(shell.CellId); + writer.Write(shell.GeometryId); + writer.Write(shell.EnvironmentId); + writer.Write(shell.CellStructure); + writer.Write(shell.Surfaces.Length); + foreach (ushort surface in shell.Surfaces) + writer.Write(surface); + WriteVector3(writer, shell.WorldPosition); + WriteQuaternion(writer, shell.Rotation); + WriteMatrix(writer, shell.Transform); + WriteVector3(writer, shell.LocalBounds.Min); + WriteVector3(writer, shell.LocalBounds.Max); + WriteVector3(writer, shell.WorldBounds.Min); + WriteVector3(writer, shell.WorldBounds.Max); + } + + private static void WritePublishedEntity(BinaryWriter writer, WorldEntity entity) + { + writer.Write(entity.Id); + writer.Write(entity.SourceGfxObjOrSetupId); + WriteVector3(writer, entity.Position); + WriteQuaternion(writer, entity.Rotation); + WriteFloat(writer, entity.Scale); + WriteNullableUInt32(writer, entity.ParentCellId); + WriteNullableUInt32(writer, entity.EffectCellId); + writer.Write(entity.IsBuildingShell); + WriteNullableUInt32(writer, entity.BuildingShellAnchorCellId); + writer.Write(entity.MeshRefs.Count); + foreach (MeshRef meshRef in entity.MeshRefs) + { + writer.Write(meshRef.GfxObjId); + WriteMatrix(writer, meshRef.PartTransform); + } + } + + private static void WriteMeshProduct( + BinaryWriter writer, + ObjectMeshData mesh, + bool cellShell, + ref int vertexCount, + ref int subsetCount, + ref int indexCount, + ref int setupPartCount) + { + writer.Write(mesh.ObjectId); + writer.Write(mesh.IsSetup); + writer.Write(mesh.Vertices.Length); + foreach (VertexPositionNormalTexture vertex in mesh.Vertices) + { + WriteVector3(writer, vertex.Position); + WriteVector3(writer, vertex.Normal); + WriteVector2(writer, vertex.UV); + } + vertexCount += mesh.Vertices.Length; + + writer.Write(mesh.Batches.Count); + foreach (MeshBatchData batch in mesh.Batches) + { + writer.Write(batch.Indices.Length); + foreach (ushort index in batch.Indices) + writer.Write(index); + writer.Write(batch.TextureFormat.Width); + writer.Write(batch.TextureFormat.Height); + writer.Write((int)batch.TextureFormat.Format); + WriteTextureKey(writer, batch.TextureKey); + writer.Write(batch.TextureIndex); + WriteNullableEnum(writer, batch.UploadPixelFormat); + WriteNullableEnum(writer, batch.UploadPixelType); + writer.Write((int)batch.CullMode); + indexCount += batch.Indices.Length; + } + + List<((int Width, int Height, TextureFormat Format) Format, TextureBatchData Batch)> subsets = + cellShell + ? ObjectMeshManager.OrderedUploadBatches(mesh) + : mesh.TextureBatches + .OrderBy(pair => pair.Key.Width) + .ThenBy(pair => pair.Key.Height) + .ThenBy(pair => (int)pair.Key.Format) + .SelectMany(pair => pair.Value.Select(batch => (pair.Key, batch))) + .ToList(); + writer.Write(subsets.Count); + foreach (var (format, batch) in subsets) + { + writer.Write(format.Width); + writer.Write(format.Height); + writer.Write((int)format.Format); + WriteTextureKey(writer, batch.Key); + WriteNullableEnum(writer, batch.UploadPixelFormat); + WriteNullableEnum(writer, batch.UploadPixelType); + writer.Write((int)batch.CullMode); + writer.Write((int)batch.Translucency); + writer.Write(batch.IsTransparent); + writer.Write(batch.IsAdditive); + writer.Write(batch.HasWrappingUVs); + WriteFloat(writer, batch.SurfaceOpacity); + writer.Write(batch.MaterialState.ToPackedByte()); + writer.Write(batch.SourceSurfaceIndex); + writer.Write(batch.RetailSurfaceMask); + writer.Write(batch.RawSurfaceType); + writer.Write(batch.IsCellShell); + writer.Write(batch.Indices.Count); + foreach (ushort index in batch.Indices) + writer.Write(index); + subsetCount++; + indexCount += batch.Indices.Count; + } + + writer.Write(mesh.SetupParts.Count); + foreach ((ulong gfxObjId, Matrix4x4 transform) in mesh.SetupParts) + { + writer.Write(gfxObjId); + WriteMatrix(writer, transform); + } + setupPartCount += mesh.SetupParts.Count; + + WriteVector3(writer, mesh.BoundingBox.Min); + WriteVector3(writer, mesh.BoundingBox.Max); + WriteVector3(writer, mesh.SortCenter); + writer.Write(mesh.DIDDegrade); + writer.Write(mesh.SelectionSphere is not null); + if (mesh.SelectionSphere is { } sphere) + { + WriteVector3(writer, sphere.Origin); + WriteFloat(writer, sphere.Radius); + } + writer.Write(mesh.EdgeLines.Length); + foreach (Vector3 edgeVertex in mesh.EdgeLines) + WriteVector3(writer, edgeVertex); + } + + private static ObjectMeshData RequireMesh( + PreparedAssetReadResult result, + string kind, + uint sourceId) + { + Assert.Equal(PreparedAssetReadStatus.Loaded, result.Status); + return Assert.IsType(result.Data); + } + + private static Vector3 PublicationOrigin(LandblockBuildOrigin origin) => new( + (((int)((LandblockId >> 24) & 0xFFu)) - origin.CenterX) * 192f, + (((int)((LandblockId >> 16) & 0xFFu)) - origin.CenterY) * 192f, + 0f); + + private static string Hash(byte[] bytes) => + Convert.ToHexString(SHA256.HashData(bytes)); + + private static void WriteVersion(BinaryWriter writer, string value) + { + byte[] bytes = System.Text.Encoding.ASCII.GetBytes(value); + writer.Write(bytes.Length); + writer.Write(bytes); + } + + private static void WriteNullableUInt32(BinaryWriter writer, uint? value) + { + writer.Write(value.HasValue); + if (value.HasValue) + writer.Write(value.Value); + } + + private static void WriteTextureKey(BinaryWriter writer, TextureKey key) + { + writer.Write(key.SurfaceId); + writer.Write(key.PaletteId); + writer.Write((int)key.Stippling); + writer.Write(key.IsSolid); + } + + private static void WriteNullableEnum(BinaryWriter writer, T? value) + where T : struct, Enum + { + writer.Write(value.HasValue); + if (value.HasValue) + writer.Write(Convert.ToInt32(value.Value)); + } + + private static void WriteVector2(BinaryWriter writer, Vector2 value) + { + WriteFloat(writer, value.X); + WriteFloat(writer, value.Y); + } + + private static void WriteVector3(BinaryWriter writer, Vector3 value) + { + WriteFloat(writer, value.X); + WriteFloat(writer, value.Y); + WriteFloat(writer, value.Z); + } + + private static void WriteQuaternion(BinaryWriter writer, Quaternion value) + { + WriteFloat(writer, value.X); + WriteFloat(writer, value.Y); + WriteFloat(writer, value.Z); + WriteFloat(writer, value.W); + } + + private static void WriteMatrix(BinaryWriter writer, Matrix4x4 value) + { + WriteFloat(writer, value.M11); + WriteFloat(writer, value.M12); + WriteFloat(writer, value.M13); + WriteFloat(writer, value.M14); + WriteFloat(writer, value.M21); + WriteFloat(writer, value.M22); + WriteFloat(writer, value.M23); + WriteFloat(writer, value.M24); + WriteFloat(writer, value.M31); + WriteFloat(writer, value.M32); + WriteFloat(writer, value.M33); + WriteFloat(writer, value.M34); + WriteFloat(writer, value.M41); + WriteFloat(writer, value.M42); + WriteFloat(writer, value.M43); + WriteFloat(writer, value.M44); + } + + private static void WriteFloat(BinaryWriter writer, float value) => + writer.Write(BitConverter.SingleToInt32Bits(value)); + + private readonly record struct Capture( + byte[] GeometryBytes, + byte[] MembershipBytes, + string GeometryHash, + string MembershipHash, + string CombinedHash, + ProductCounts Counts); + + private readonly record struct ProductFacts( + uint RampEntityId, + ulong ShellGeometryId, + uint CrossOwnerCellId); + + private readonly record struct GeometryCounts( + int VisibilityCells, + int Shells, + int PublishedEntities, + int SetupMeshes, + int GfxObjMeshes, + int Vertices, + int Subsets, + int Indices, + int SetupParts); + + private readonly record struct MembershipCounts( + int PublishedEntities, + int LogicalOwners, + int RetainedCellArrays, + int RetainedCellArrayCells, + int CellDomain, + int PartEntries); + + private readonly record struct ProductCounts( + GeometryCounts Geometry, + MembershipCounts Membership); + + private enum RetailPlaneSide + { + Positive = 0, + Negative = 1, + InPlane = 2, + Crossing = 3, + } + + private readonly record struct RetailBoxClassification( + RetailPlaneSide Side, + IReadOnlyList CornerSides, + double EpsilonTieMargin); + + /// + /// Test-only installed-DAT implementation of the immutable prepared + /// collision source. It performs only the existing flat conversions; + /// membership remains exclusively owned by PublishStaticCollision and + /// ShadowObjectRegistry. + /// + private sealed class InstalledDatPreparedCollisionAdapter : IPreparedCollisionSource + { + private readonly IDatReaderWriter _dats; + private long _probes; + private long _reads; + private long _loaded; + private long _missing; + + public InstalledDatPreparedCollisionAdapter(IDatReaderWriter dats) => + _dats = dats; + + public PreparedCollisionSourceStats CollisionStats => new( + _probes, + _reads, + _loaded, + _missing, + Corrupt: 0); + + public PreparedAssetPresence ProbeCollision(PakAssetType type, uint sourceFileId) + { + _probes++; + bool available = type switch + { + PakAssetType.GfxObjCollision => _dats.Get(sourceFileId) is not null, + PakAssetType.SetupCollision => _dats.Get(sourceFileId) is not null, + PakAssetType.CellStructureCollision => + TryResolveCell(sourceFileId, out _, out _), + PakAssetType.EnvCellTopology => + TryResolveCell(sourceFileId, out _, out _), + _ => throw new ArgumentOutOfRangeException(nameof(type)), + }; + return available + ? PreparedAssetPresence.Available + : PreparedAssetPresence.Missing; + } + + public PreparedCollisionReadResult ReadGfxObjCollision( + uint sourceFileId, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + _reads++; + DatGfxObj? value = _dats.Get(sourceFileId); + if (value is null) + return Missing(); + return Loaded(FlatCollisionAssetBuilder.FlattenGfxObj(value)); + } + + public PreparedCollisionReadResult ReadSetupCollision( + uint sourceFileId, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + _reads++; + DatSetup? value = _dats.Get(sourceFileId); + if (value is null) + return Missing(); + return Loaded(FlatCollisionAssetBuilder.FlattenSetup(value)); + } + + public PreparedCollisionReadResult + ReadCellStructureCollision( + uint sourceFileId, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + _reads++; + if (!TryResolveCell(sourceFileId, out _, out var structure)) + return Missing(); + return Loaded(FlatCollisionAssetBuilder.FlattenCellStructure(structure)); + } + + public PreparedCollisionReadResult ReadEnvCellTopology( + uint sourceFileId, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + _reads++; + if (!TryResolveCell(sourceFileId, out DatEnvCell? cell, out var structure)) + return Missing(); + FlatCellStructureCollisionAsset flat = + FlatCollisionAssetBuilder.FlattenCellStructure(structure); + return Loaded(FlatCollisionAssetBuilder.FlattenEnvCellTopology( + sourceFileId, + cell!, + flat.PortalPolygons)); + } + + private bool TryResolveCell( + uint sourceFileId, + out DatEnvCell? cell, + out DatReaderWriter.Types.CellStruct structure) + { + cell = _dats.Get(sourceFileId); + if (cell is not null + && _dats.Get(0x0D000000u | cell.EnvironmentId) is { } environment + && environment.Cells.TryGetValue(cell.CellStructure, out var found) + && found is not null) + { + structure = found; + return true; + } + structure = null!; + return false; + } + + private PreparedCollisionReadResult Loaded(T value) + where T : class + { + _loaded++; + return PreparedCollisionReadResult.Loaded(value); + } + + private PreparedCollisionReadResult Missing() + where T : class + { + _missing++; + return PreparedCollisionReadResult.Missing; + } + + public void Dispose() + { + } + } +}