feat(phys): A6.P3 #98 Step 2 — cell-dump probe + roundtrip test

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).
This commit is contained in:
Erik 2026-05-23 15:16:56 +02:00
parent 35b37dfb5f
commit f62a873be3
4 changed files with 484 additions and 1 deletions

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Numerics;
namespace AcDream.Core.Physics;
@ -395,6 +396,63 @@ public static class PhysicsDiagnostics
public static bool ProbeStepWalkEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_STEP_WALK") == "1";
/// <summary>
/// A6.P3 issue #98 cell-dump probe (2026-05-23). When non-empty, dumps
/// the geometry of any cached cell whose EnvCellId matches one of the
/// listed ids to a JSON file under
/// <see cref="ProbeDumpCellsPath"/>. One-shot per cell (a second cache
/// of the same id is a no-op).
///
/// <para>
/// Configured via <c>ACDREAM_DUMP_CELLS</c> as a comma-separated list of
/// hex cell ids (with or without <c>0x</c> prefix). The output path
/// defaults to
/// <c>tests/AcDream.Core.Tests/Fixtures/issue98/&lt;cellid&gt;.json</c>
/// (relative to the worktree root). Override with
/// <c>ACDREAM_DUMP_CELLS_DIR=&lt;dir&gt;</c>.
/// </para>
///
/// <para>
/// The dump fuels the deterministic replay harness for issue #98 — the
/// goal is to load cells 0xA9B40143 + 0xA9B40146 + 0xA9B40147 as JSON
/// fixtures and drive the failing-frame sphere through the walkable
/// query in a unit test, eliminating live-client iteration cost from the
/// investigation loop.
/// </para>
/// </summary>
public static IReadOnlySet<uint> ProbeDumpCellIds { get; set; } =
ParseHexIdList(Environment.GetEnvironmentVariable("ACDREAM_DUMP_CELLS"));
public static string ProbeDumpCellsPath { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_DUMP_CELLS_DIR")
?? "tests/AcDream.Core.Tests/Fixtures/issue98";
public static bool ProbeDumpCellsEnabled => ProbeDumpCellIds.Count > 0;
private static IReadOnlySet<uint> ParseHexIdList(string? raw)
{
if (string.IsNullOrWhiteSpace(raw))
return new System.Collections.Generic.HashSet<uint>();
var ids = new System.Collections.Generic.HashSet<uint>();
foreach (var token in raw.Split(',', StringSplitOptions.RemoveEmptyEntries))
{
var trimmed = token.Trim();
if (trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
trimmed = trimmed[2..];
if (uint.TryParse(
trimmed,
System.Globalization.NumberStyles.HexNumber,
System.Globalization.CultureInfo.InvariantCulture,
out var id))
{
ids.Add(id);
}
}
return ids;
}
/// <summary>
/// Side-channel populated by <c>BSPQuery.SphereIntersectsSolidInternal</c>
/// at the leaf where it returns true. Either