using System.Collections.Generic; using System.IO; using System.Numerics; using AcDream.Core.Physics; using DatReaderWriter.Enums; using Xunit; namespace AcDream.Core.Tests.Physics; /// /// A6.P3 issue #98 (2026-05-23). Sanity check for /// : hand-construct a small /// , snapshot it, serialize → deserialize → hydrate, /// then assert the round-trip preserves every field the replay harness /// depends on. /// public class CellDumpRoundTripTests { [Fact] public void Capture_Then_Hydrate_PreservesTransformsAndPolygons() { var original = MakeFixtureCell(); var dump = CellDumpSerializer.Capture(0xA9B40147u, original); var hydrated = CellDumpSerializer.Hydrate(dump); Assert.Equal(original.WorldTransform, hydrated.WorldTransform); Assert.Equal(original.InverseWorldTransform, hydrated.InverseWorldTransform); Assert.Equal(original.Resolved.Count, hydrated.Resolved.Count); foreach (var (id, originalPoly) in original.Resolved) { Assert.True(hydrated.Resolved.TryGetValue(id, out var rehydrated)); Assert.Equal(originalPoly.NumPoints, rehydrated!.NumPoints); Assert.Equal(originalPoly.SidesType, rehydrated.SidesType); Assert.Equal(originalPoly.Plane.Normal, rehydrated.Plane.Normal); Assert.Equal(originalPoly.Plane.D, rehydrated.Plane.D); Assert.Equal(originalPoly.Vertices.Length, rehydrated.Vertices.Length); for (int i = 0; i < originalPoly.Vertices.Length; i++) Assert.Equal(originalPoly.Vertices[i], rehydrated.Vertices[i]); } } [Fact] public void Capture_Then_Hydrate_PreservesPortalsAndVisibleCells() { var original = MakeFixtureCell(); var dump = CellDumpSerializer.Capture(0xA9B40147u, original); var hydrated = CellDumpSerializer.Hydrate(dump); Assert.Equal(original.Portals.Count, hydrated.Portals.Count); for (int i = 0; i < original.Portals.Count; i++) { Assert.Equal(original.Portals[i].OtherCellId, hydrated.Portals[i].OtherCellId); Assert.Equal(original.Portals[i].PolygonId, hydrated.Portals[i].PolygonId); Assert.Equal(original.Portals[i].Flags, hydrated.Portals[i].Flags); } Assert.Equal(original.VisibleCellIds.Count, hydrated.VisibleCellIds.Count); foreach (var id in original.VisibleCellIds) Assert.Contains(id, hydrated.VisibleCellIds); } [Fact] public void WriteRead_OnDisk_PreservesContent() { var original = MakeFixtureCell(); var dump = CellDumpSerializer.Capture(0xA9B40147u, original); var path = Path.Combine(Path.GetTempPath(), $"acdream-celldump-test-{System.Guid.NewGuid():N}.json"); try { CellDumpSerializer.Write(dump, path); Assert.True(File.Exists(path)); var readBack = CellDumpSerializer.Read(path); Assert.Equal(dump.CellId, readBack.CellId); Assert.Equal(dump.ResolvedPolygons.Count, readBack.ResolvedPolygons.Count); Assert.Equal(dump.Portals.Count, readBack.Portals.Count); var hydrated = CellDumpSerializer.Hydrate(readBack); Assert.Equal(original.WorldTransform, hydrated.WorldTransform); Assert.Equal(original.Resolved.Count, hydrated.Resolved.Count); } finally { if (File.Exists(path)) File.Delete(path); } } [Fact] public void Hydrate_LeavesBspNullsSoLeafLevelPredicatesAreTheReplayPath() { var original = MakeFixtureCell(); var dump = CellDumpSerializer.Capture(0xA9B40147u, original); var hydrated = CellDumpSerializer.Hydrate(dump); // Replay harness drives leaf-level predicates directly on // Resolved; the BSP trees aren't reconstructed in this iteration. Assert.Null(hydrated.BSP); Assert.Null(hydrated.CellBSP); Assert.NotEmpty(hydrated.Resolved); } private static CellPhysics MakeFixtureCell() { // Triangle floor at local Z=0 with verts roughly matching poly // 0x0004 from the issue #98 trace (in 0xA9B40143's frame). var resolved = new Dictionary { [0x0004] = new ResolvedPolygon { Id = 0x0004, NumPoints = 3, SidesType = CullMode.Clockwise, Plane = new Plane(new Vector3(0, 0, 1), 0f), Vertices = new[] { new Vector3(-6.2f, 7.6f, 0f), new Vector3(-10.0f, 7.6f, 0f), new Vector3(-10.0f, 2.8f, 0f), }, }, [0x0008] = new ResolvedPolygon { Id = 0x0008, NumPoints = 4, SidesType = CullMode.Clockwise, Plane = new Plane(new Vector3(0f, -0.7190f, 0.6950f), -0.1007f), Vertices = new[] { new Vector3( 0.8f, -1.59f, -1.5f), new Vector3( 0.8f, 1.31f, 1.5f), new Vector3(-0.8f, 1.31f, 1.5f), new Vector3(-0.8f, -1.59f, -1.5f), }, }, }; var portalPolys = new Dictionary { [0x0100] = new ResolvedPolygon { Id = 0x0100, NumPoints = 4, SidesType = CullMode.Clockwise, Plane = new Plane(new Vector3(1, 0, 0), 5f), Vertices = new[] { new Vector3(5f, -1f, -1f), new Vector3(5f, 1f, -1f), new Vector3(5f, 1f, 1f), new Vector3(5f, -1f, 1f), }, }, }; var portals = new List { new(otherCellId: 0x0146, polygonId: 0x0100, flags: 0x0002), }; var visible = new HashSet { 0xA9B40143u, 0xA9B40146u }; var worldTransform = Matrix4x4.CreateRotationZ(MathF.PI) * Matrix4x4.CreateTranslation(130.5f, 11.5f, 94.0f); Matrix4x4.Invert(worldTransform, out var inverse); return new CellPhysics { BSP = null, PhysicsPolygons = null, Vertices = null, WorldTransform = worldTransform, InverseWorldTransform = inverse, Resolved = resolved, CellBSP = null, Portals = portals, PortalPolygons = portalPolys, VisibleCellIds = visible, }; } }