Step 2 of the apparatus plan at C:\Users\erikn\.claude\plans\i-did-some-work-sharded-acorn.md. Adds a one-shot cell-dump probe so the issue #98 replay harness can load real cellar / cottage geometry as JSON fixtures, eliminating live-client iteration from every fix attempt. Probe gate: ACDREAM_DUMP_CELLS=0xA9B40143,0xA9B40146,0xA9B40147 ACDREAM_DUMP_CELLS_DIR=tests/AcDream.Core.Tests/Fixtures/issue98 (default) When set, the first time PhysicsDataCache.CacheCellStruct sees a matching envCellId, it serializes the resulting CellPhysics to <dir>/0x<cellid>.json and prints one [cell-dump] line. Zero cost when unset (gate is a static-readonly IReadOnlySet<uint>.Count check). DTOs (CellDump.cs): - CellDump: top-level record holding cell id, WorldTransform, InverseWorldTransform, resolved polygons, portal polygons, portal infos, visible cell ids. - PolygonDump / PortalDump / Vector3Dto / PlaneDto / Matrix4x4Dto: System.Text.Json-friendly records with explicit From / To converters. What is intentionally NOT dumped: the DAT-native PhysicsBSPTree and CellBSPTree trees. The replay harness drives the leaf-level walkable predicates (WalkableHitsSphere, FindCrossedEdge, PolygonHitsSpherePrecise) directly on the resolved polygon list, which is enough to expose the issue #98 rejection (poly 0x0004 in 0xA9B40143 reports insideEdges=False / overlapsSphere=False at the failing-frame sphere). If a future replay needs BSP traversal we can extend the DTO + Hydrate together without breaking fixtures. Tests (CellDumpRoundTripTests): - Capture → Hydrate preserves WorldTransform / InverseWorldTransform / every polygon's plane + vertices + NumPoints + SidesType. - Capture → Hydrate preserves portal list + visible cell ids. - Write to disk → Read back → Hydrate preserves content. - Hydrate leaves BSP / CellBSP null by design (replay uses leaf-level predicates). Verification: - dotnet build: green, 0 errors. - dotnet test: 1160 passed + 8 pre-existing failed (was 1156 + 8 before this commit; +4 from CellDumpRoundTripTests). Same 8 pre-existing failures, no new regressions. Next: capture the three cells from the live client (Step 2 acceptance), then build the replay harness against the fixtures (Step 3).
186 lines
6.8 KiB
C#
186 lines
6.8 KiB
C#
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Numerics;
|
|
using AcDream.Core.Physics;
|
|
using DatReaderWriter.Enums;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Tests.Physics;
|
|
|
|
/// <summary>
|
|
/// A6.P3 issue #98 (2026-05-23). Sanity check for
|
|
/// <see cref="CellDumpSerializer"/>: hand-construct a small
|
|
/// <see cref="CellPhysics"/>, snapshot it, serialize → deserialize → hydrate,
|
|
/// then assert the round-trip preserves every field the replay harness
|
|
/// depends on.
|
|
/// </summary>
|
|
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<ushort, ResolvedPolygon>
|
|
{
|
|
[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<ushort, ResolvedPolygon>
|
|
{
|
|
[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<PortalInfo>
|
|
{
|
|
new(otherCellId: 0x0146, polygonId: 0x0100, flags: 0x0002),
|
|
};
|
|
|
|
var visible = new HashSet<uint> { 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,
|
|
};
|
|
}
|
|
}
|