The moving-fixture divergence was the building degrade ladder: GfxObjDegradeInfo::get_degrade @0x0051e4b0 (Ghidra-verified - BN's FPU-flag pseudo-C misread BOTH arm selection and one formula) slides each level's threshold from ideal toward MAX as the multiplier approaches 1, and the live client runs the positive arm at ~0.99, so level 0's portal-bearing BSP survives to ~max_dist (48 for the Holtburg cottages), not ideal (24). The recon note's "deg_mul = -0.99" was a sign misread; the negative arm's threshold slides toward MIN and contradicts the fixtures from both directions. With the two-arm port, holtburg-walkout, holtburg-transitions, and holtburg-walkabout pass every pairable frame - the walkout-F2 microscope's prediction (103,100 | 100x3 | 124 through the cottage exit views) landed exactly. Also this round, falsified and reverted: a BN-driven swap of the portal walker's negative/in-plane arms (Ghidra shows side 1 = negative EMITS, side 2 = in-plane does not - the original port was correct; the swap broke four fixtures). The walker docs and unit tests now pin the Ghidra-verified truth table. Parked with findings: foundry-entry F67 (right flood set, one plane-side classification at the +/-eps boundary orders 11d before 116/118) and doorway-still (retail shows zero floods at a pose one meter from walkout-F2's flooding pose; multi-portal clip boundary). Suites: Walk 124/3 skips; hermetic 6,687/0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
229 lines
11 KiB
C#
229 lines
11 KiB
C#
using AcDream.App.Tests.Rendering;
|
||
using AcDream.App.Rendering.Walk;
|
||
using DatReaderWriter;
|
||
using DatReaderWriter.Options;
|
||
using System.Numerics;
|
||
using System.Text;
|
||
|
||
namespace AcDream.App.Tests.Rendering.Walk;
|
||
|
||
/// <summary>
|
||
/// FW1 look-in gate microscope: for the street fixture pose, dump every
|
||
/// gate input for building a9b4001a (retail punches it) vs a9b4001e
|
||
/// (retail does not): raw portal flags, the BSP-emitted polygon's plane,
|
||
/// the eye's signed distance and side, and the active-view clip count.
|
||
/// Writes the dump to the scratch directory; always passes.
|
||
/// </summary>
|
||
[Trait("Lane", "InstalledDat")]
|
||
public sealed class WalkPortalGateDumpTests
|
||
{
|
||
[Fact]
|
||
public void Dump_the_gate_inputs_for_a_punching_and_a_silent_building()
|
||
{
|
||
string? datDir = CornerFloodReplayTests.ResolveDatDir();
|
||
if (datDir is null)
|
||
{
|
||
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory.");
|
||
}
|
||
IReadOnlyList<WalkOracleFrame> frames =
|
||
WalkOracleTrace.Load("posed/holtburg-street-outdoor");
|
||
WalkOracleFrame frame = frames[1];
|
||
Assert.NotNull(frame.Pose);
|
||
using var dats = new DatCollection(datDir!, DatAccessType.Read);
|
||
WalkLandscapeDatBuilder.BuiltWorld world =
|
||
WalkLandscapeDatBuilder.Build(dats, frame.Pose!.CellId, frame.Pose.Origin);
|
||
var ctx = new WalkTraceReplayContext(frame.Pose, world.Cells)
|
||
{
|
||
Buildings = world.Buildings,
|
||
};
|
||
// Install the outdoor default view as the active clip context.
|
||
var defaultView = new WalkPortalView();
|
||
WalkCopyView.AppendFullViewportQuad(
|
||
defaultView, ctx.Rays, ctx.WorldViewpoint,
|
||
ctx.ViewportWidth, ctx.ViewportHeight);
|
||
ctx.SetActiveView(defaultView, 0);
|
||
|
||
var dump = new StringBuilder();
|
||
Span<WalkScreenPoint> clipped = stackalloc WalkScreenPoint[64];
|
||
dump.AppendLine($"pose cell={frame.Pose.CellId:x8} origin={frame.Pose.Origin}");
|
||
foreach (uint id in new[] { 0xA9B4001Au, 0xA9B4001Eu, 0xA9B40022u, 0xA9B40026u })
|
||
{
|
||
WalkBuilding building = world.Buildings.Keys.Single(b => b.PositionCellId == id);
|
||
float dist = ctx.ViewerDistanceTo(building);
|
||
Vector3 eyeInBuilding = ctx.ViewpointInBuilding(building);
|
||
dump.AppendLine(
|
||
$"building {id:x8}: dist={dist:f1} eff={MathF.Max(0f, dist - 100f):f1} "
|
||
+ $"eyeLocal={eyeInBuilding} portals={building.Portals.Length}");
|
||
WalkBspNode? bsp = building.SelectDrawingBsp(dist);
|
||
var refs = new List<WalkPortalRef>();
|
||
Collect(bsp, refs);
|
||
dump.AppendLine($" level BSP portal refs: {refs.Count}");
|
||
foreach (WalkPortalRef portalRef in refs)
|
||
{
|
||
ref WalkBldPortal bp = ref building.Portals[portalRef.PortalIndex];
|
||
float d = Vector3.Dot(portalRef.Polygon.Plane.Normal, eyeInBuilding)
|
||
+ portalRef.Polygon.Plane.D;
|
||
int side = d > WalkVisibilityMath.Epsilon ? 0
|
||
: d < -WalkVisibilityMath.Epsilon ? 1 : 2;
|
||
int n = ctx.ClipBuildingPolygon(building, portalRef.Polygon, side, clipped);
|
||
dump.AppendLine(
|
||
$" ref idx={portalRef.PortalIndex} other={bp.OtherCellId:x8} "
|
||
+ $"rawSide={bp.PortalSide} exact={bp.ExactMatch} "
|
||
+ $"planeD@eye={d:f2} eyeSide={side} clipN={n} "
|
||
+ $"gate(side==rawSide)={(side == bp.PortalSide ? "PASS" : "reject")}");
|
||
}
|
||
}
|
||
string path = Path.Combine(Path.GetTempPath(), "fw1-portal-gate-dump.txt");
|
||
File.WriteAllText(path, dump.ToString());
|
||
Assert.True(true);
|
||
}
|
||
|
||
private static void Collect(WalkBspNode? node, List<WalkPortalRef> into)
|
||
{
|
||
if (node is null) return;
|
||
if (node.InPortals is not null) into.AddRange(node.InPortals);
|
||
Collect(node.PosNode, into);
|
||
Collect(node.NegNode, into);
|
||
}
|
||
|
||
private sealed class NullSink : IWalkEventSink
|
||
{
|
||
public void Emit(in WalkEvent walkEvent) { }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Walkout-F2 microscope: retail floods buildings a9b4001e (×4) and
|
||
/// a9b40026 (×1) through the cottage's exit views; the replay floods
|
||
/// neither, while the SAME machinery against the root view passes the
|
||
/// street fixture. Differential per portal: clip count against the
|
||
/// full-viewport window vs each exit-view window, plus each window's
|
||
/// vertices and signed area (winding).
|
||
/// </summary>
|
||
[Fact]
|
||
public void Dump_the_exit_view_look_in_inputs_for_walkout_f2()
|
||
{
|
||
string? datDir = CornerFloodReplayTests.ResolveDatDir();
|
||
if (datDir is null)
|
||
{
|
||
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory.");
|
||
}
|
||
IReadOnlyList<WalkOracleFrame> frames =
|
||
WalkOracleTrace.Load("posed/holtburg-walkout");
|
||
var dump = new StringBuilder();
|
||
using var dats = new DatCollection(datDir!, DatAccessType.Read);
|
||
// Marker lag: F2's true camera lies between pose(F2) and pose(F3).
|
||
foreach (WalkOraclePose pose in new[] { frames[2].Pose!, frames[1].Pose! })
|
||
{
|
||
WalkLandscapeDatBuilder.BuiltWorld world =
|
||
WalkLandscapeDatBuilder.Build(dats, pose.CellId, pose.Origin);
|
||
var ctx = new WalkTraceReplayContext(pose, world.Cells)
|
||
{
|
||
Buildings = world.Buildings,
|
||
};
|
||
WalkCell camera = world.Cells[pose.CellId];
|
||
var walk = new RetailFrameWalk();
|
||
walk.WalkFrame(pose.CellId, camera, world.Landscape, ctx, new NullSink());
|
||
|
||
WalkPortalView outside = walk.InteriorPView.OutsideView;
|
||
dump.AppendLine(
|
||
$"pose cell={pose.CellId:x8} origin={pose.Origin} ov={outside.ViewCount}");
|
||
for (int v = 0; v < outside.ViewCount; v++)
|
||
{
|
||
WalkViewPoly poly = outside.View.Polys[v];
|
||
var verts = new Vector2[poly.VertexCount];
|
||
for (int k = 0; k < poly.VertexCount; k++)
|
||
verts[k] = outside.View.Vertices[poly.VertexIndex + k].Point;
|
||
dump.AppendLine(
|
||
$" exit view {v}: n={poly.VertexCount} area={SignedArea(verts):f1} "
|
||
+ $"verts={string.Join(' ', verts.Select(p => $"({p.X:f1},{p.Y:f1})"))}");
|
||
}
|
||
var defaultView = new WalkPortalView();
|
||
WalkCopyView.AppendFullViewportQuad(
|
||
defaultView, ctx.Rays, ctx.WorldViewpoint,
|
||
ctx.ViewportWidth, ctx.ViewportHeight);
|
||
{
|
||
WalkViewPoly rootPoly = defaultView.View.Polys[0];
|
||
var rootVerts = new Vector2[rootPoly.VertexCount];
|
||
for (int k = 0; k < rootPoly.VertexCount; k++)
|
||
rootVerts[k] = defaultView.View.Vertices[rootPoly.VertexIndex + k].Point;
|
||
dump.AppendLine(
|
||
$" root view: n={rootPoly.VertexCount} area={SignedArea(rootVerts):f1} "
|
||
+ $"verts={string.Join(' ', rootVerts.Select(p => $"({p.X:f1},{p.Y:f1})"))}");
|
||
}
|
||
|
||
foreach (uint id in new[] { 0xA9B4001Eu, 0xA9B40026u })
|
||
{
|
||
WalkBuilding building =
|
||
world.Buildings.Keys.Single(b => b.PositionCellId == id);
|
||
float dist = ctx.ViewerDistanceTo(building);
|
||
Vector3 eyeInBuilding = ctx.ViewpointInBuilding(building);
|
||
WalkBspNode? bsp = building.SelectDrawingBsp(dist);
|
||
dump.AppendLine(
|
||
$" building {id:x8}: dist={dist:f1} eff={MathF.Max(0f, dist - 100f):f1} "
|
||
+ $"bsp={(bsp is null ? "NULL" : "selected")} ports={CountPorts(bsp)}");
|
||
for (int li = 0; li < building.DegradeLevels.Length; li++)
|
||
{
|
||
WalkBuildingDegradeLevel lv = building.DegradeLevels[li];
|
||
dump.AppendLine(
|
||
$" level {li}: min={lv.MinDist:f1} ideal={lv.IdealDist:f1} "
|
||
+ $"max={lv.MaxDist:f1} ports={CountPorts(lv.DrawingBsp)} "
|
||
+ $"idealArmThr={lv.IdealDist:f1} "
|
||
+ $"negMulThr={lv.IdealDist - (lv.IdealDist - lv.MaxDist) * -0.99f:f1}");
|
||
}
|
||
if (bsp is null) continue;
|
||
var refs = new List<WalkPortalRef>();
|
||
WalkBuildingPortals.BuildDrawPortalsOnly(
|
||
bsp, 1, eyeInBuilding, (portalRef, _) => refs.Add(portalRef));
|
||
var clipped = new WalkScreenPoint[64];
|
||
foreach (WalkPortalRef portalRef in refs)
|
||
{
|
||
ref WalkBldPortal bp = ref building.Portals[portalRef.PortalIndex];
|
||
float d = Vector3.Dot(portalRef.Polygon.Plane.Normal, eyeInBuilding)
|
||
+ portalRef.Polygon.Plane.D;
|
||
int side = d > WalkVisibilityMath.Epsilon ? 0
|
||
: d < -WalkVisibilityMath.Epsilon ? 1 : 2;
|
||
ctx.SetActiveView(defaultView, 0);
|
||
int rootN = ctx.ClipBuildingPolygon(
|
||
building, portalRef.Polygon, side, clipped);
|
||
string rootVerts = string.Join(
|
||
' ',
|
||
clipped.Take(rootN).Select(p => $"({p.X:f1},{p.Y:f1},w={p.W:f3})"));
|
||
var perView = new StringBuilder();
|
||
for (int v = 0; v < outside.ViewCount; v++)
|
||
{
|
||
ctx.SetActiveView(outside, v);
|
||
int n = ctx.ClipBuildingPolygon(
|
||
building, portalRef.Polygon, side, clipped);
|
||
perView.Append($" view{v}N={n}");
|
||
}
|
||
dump.AppendLine(
|
||
$" ref idx={portalRef.PortalIndex} other={bp.OtherCellId:x8} "
|
||
+ $"rawSide={bp.PortalSide} eyeSide={side} "
|
||
+ $"gate={(side == bp.PortalSide ? "PASS" : "REJECT")} "
|
||
+ $"rootN={rootN}{perView}");
|
||
if (rootN > 0)
|
||
dump.AppendLine($" root-clipped: {rootVerts}");
|
||
}
|
||
}
|
||
}
|
||
string path = Path.Combine(Path.GetTempPath(), "fw1-walkout-f2-lookin-dump.txt");
|
||
File.WriteAllText(path, dump.ToString());
|
||
Assert.True(true);
|
||
}
|
||
|
||
private static int CountPorts(WalkBspNode? node)
|
||
=> node is null ? 0
|
||
: (node.IsPortal ? 1 : 0) + CountPorts(node.PosNode) + CountPorts(node.NegNode);
|
||
|
||
private static float SignedArea(Vector2[] verts)
|
||
{
|
||
float sum = 0f;
|
||
for (int i = 0; i < verts.Length; i++)
|
||
{
|
||
Vector2 a = verts[i];
|
||
Vector2 b = verts[(i + 1) % verts.Length];
|
||
sum += a.X * b.Y - b.X * a.Y;
|
||
}
|
||
return 0.5f * sum;
|
||
}
|
||
}
|