acdream/tests/AcDream.App.Tests/Rendering/Walk/WalkBuildingPortalTests.cs
Erik c1a029edf9 feat(render) Campaign FW1: NINE of ten fixtures conformant - the degrade arm
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>
2026-08-30 11:34:45 +02:00

218 lines
8.4 KiB
C#

using System.Numerics;
using AcDream.App.Rendering.Walk;
namespace AcDream.App.Tests.Rendering.Walk;
public sealed class WalkBuildingPortalTests
{
private sealed class RecordingSink : WalkBuildingPortals.IWalkPortalPassSink
{
public readonly List<WalkPolygon> Punches = new();
public readonly List<uint[]> DrawCells = new();
public void OnPunch(WalkPolygon polygon) => Punches.Add(polygon);
public void OnDrawCells(WalkPView pview)
=> DrawCells.Add(pview.CellDrawList.Select(c => c.CellId).ToArray());
}
private sealed class Caster : IWalkRayCaster
{
public Vector3 RayThrough(float screenX, float screenY)
=> new(screenX, screenY, 100f);
}
private sealed class TestContext : IWalkFrameContext, IWalkBuildingFrameContext
{
public readonly Dictionary<uint, WalkCell> Cells = new();
private readonly Matrix4x4 _viewProj;
private static readonly Vector2[] RootQuad =
[
new(0, 480), new(640, 480), new(640, 0), new(0, 0),
];
public TestContext()
{
Matrix4x4 view = Matrix4x4.CreateLookAt(
Vector3.Zero, new Vector3(0, 0, -1), Vector3.UnitY);
Matrix4x4 proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1f, 0.1f, 1000f);
_viewProj = view * proj;
}
public Vector3 ViewpointIn(WalkCell cell) => Vector3.Zero;
public Matrix4x4 ObjectToClip(WalkCell cell) => _viewProj;
public WalkCell? GetVisible(uint cellId) => Cells.GetValueOrDefault(cellId);
public IWalkRayCaster Rays { get; } = new Caster();
public Vector3 WorldViewpoint => Vector3.Zero;
public float ViewportWidth => 640f;
public float ViewportHeight => 480f;
public Vector3 ViewpointInBuilding(WalkBuilding building) => Vector3.Zero;
public float ViewerDistanceTo(WalkBuilding building) => 0f;
public IWalkFrameContext CellContext => this;
public int ClipBuildingPolygon(
WalkBuilding building, WalkPolygon polygon, int side, Span<WalkScreenPoint> output)
{
Span<WalkScreenPoint> projected = stackalloc WalkScreenPoint[polygon.Vertices.Length];
for (int i = 0; i < polygon.Vertices.Length; i++)
projected[i] = WalkScreenClip.TransformToScreen(
polygon.Vertices[i], _viewProj, ViewportWidth, ViewportHeight);
if (side != 0)
projected.Reverse();
return WalkScreenClip.ClipAgainstView(projected, RootQuad, output);
}
}
private static WalkPolygon Quad(float z, float half = 0.5f, bool facingViewer = true) => new()
{
Vertices =
[
new Vector3(-half, -half, z), new Vector3(half, -half, z),
new Vector3(half, half, z), new Vector3(-half, half, z),
],
Plane = new WalkPlane(new Vector3(0, 0, facingViewer ? 1f : -1f), facingViewer ? -z : z),
};
private static WalkBspNode PortalNode(WalkPlane plane, params WalkPortalRef[] portals)
=> new() { SplittingPlane = plane, InPortals = portals };
// ---- the BSP portal-only walk ----
[Fact]
public void Bsp_walk_emits_the_far_side_first()
{
var farPortal = new WalkPortalRef { PortalIndex = 0, Polygon = Quad(-4f) };
var nearPortal = new WalkPortalRef { PortalIndex = 1, Polygon = Quad(-2f) };
// Splitting plane x = 0; viewer at x = +5 (side 0) → NEG child first.
var root = new WalkBspNode
{
SplittingPlane = new WalkPlane(new Vector3(1, 0, 0), 0f),
NegNode = PortalNode(new WalkPlane(new Vector3(0, 0, 1), 100f), farPortal),
PosNode = PortalNode(new WalkPlane(new Vector3(0, 0, 1), 100f), nearPortal),
};
var emitted = new List<int>();
WalkBuildingPortals.BuildDrawPortalsOnly(
root, 1, new Vector3(5, 0, 0), (p, _) => emitted.Add(p.PortalIndex));
Assert.Equal(new[] { 0, 1 }, emitted);
emitted.Clear();
WalkBuildingPortals.BuildDrawPortalsOnly(
root, 1, new Vector3(-5, 0, 0), (p, _) => emitted.Add(p.PortalIndex));
Assert.Equal(new[] { 1, 0 }, emitted);
}
[Fact]
public void In_plane_portal_node_emits_nothing()
{
// BSPPORTAL::portal_draw_portals_only @0053d870, GHIDRA-verified
// 2026-08-30: |d| ≤ ε (side 2) visits POS and emits NOTHING; the
// NEGATIVE arm (side 1) is the one that emits. (A BN flag-mush
// re-read briefly swapped these arms and broke four fixtures —
// never swap them again without Ghidra + fixture proof.)
var portal = new WalkPortalRef { PortalIndex = 0, Polygon = Quad(-2f) };
// Viewer exactly on the node's splitting plane (|d| <= epsilon).
WalkBspNode root = PortalNode(new WalkPlane(new Vector3(1, 0, 0), 0f), portal);
var emitted = new List<int>();
WalkBuildingPortals.BuildDrawPortalsOnly(
root, 1, Vector3.Zero, (p, _) => emitted.Add(p.PortalIndex));
Assert.Empty(emitted);
}
// ---- ConstructView(CBldPortal) + DrawPortal ----
private static (TestContext ctx, WalkBuilding building, WalkCell interior, WalkPortalRef portalRef)
BuildLookInFixture(int portalSide = 0)
{
var ctx = new TestContext();
var interior = new WalkCell
{
CellId = 0x104,
Portals = [new WalkCellPortal
{
OtherCellId = 0xFFFFFFFF, PolygonIndex = 0, PortalSide = 1, OtherPortalId = 0,
}],
PortalPolygons = [Quad(-2f)],
};
ctx.Cells[interior.CellId] = interior;
var building = new WalkBuilding
{
PositionCellId = 0xA9B4000Fu,
Portals =
[
new WalkBldPortal
{
PortalSide = portalSide, OtherCellId = 0x104, OtherPortalId = 0,
StabList = [0x104u],
},
],
};
var portalRef = new WalkPortalRef { PortalIndex = 0, Polygon = Quad(-2f) };
return (ctx, building, interior, portalRef);
}
[Fact]
public void Pass_one_punches_and_appends_the_view_without_flooding()
{
(TestContext ctx, WalkBuilding building, WalkCell interior, WalkPortalRef portalRef)
= BuildLookInFixture();
var pview = new WalkPView();
var sink = new RecordingSink();
bool ok = WalkBuildingPortals.DrawPortal(pview, building, portalRef, 1, ctx, sink);
Assert.True(ok);
Assert.Single(sink.Punches);
Assert.Empty(sink.DrawCells);
Assert.Empty(pview.CellDrawList);
Assert.Equal(0, interior.NumView); // stab views popped back
}
[Fact]
public void Pass_two_floods_the_interior_and_emits_the_draw_cells_event()
{
(TestContext ctx, WalkBuilding building, WalkCell interior, WalkPortalRef portalRef)
= BuildLookInFixture();
var pview = new WalkPView();
var sink = new RecordingSink();
bool ok = WalkBuildingPortals.DrawPortal(pview, building, portalRef, 2, ctx, sink);
Assert.True(ok);
Assert.Empty(sink.Punches); // pass 2 never draws the poly
Assert.Single(sink.DrawCells);
Assert.Equal(new[] { 0x104u }, sink.DrawCells[0]);
}
[Fact]
public void Wrong_viewer_side_rejects_the_look_in()
{
// portal_side = 1 requires side NEGATIVE; the fixture eye computes
// side POSITIVE → rejected: no punch, no view, no flood.
(TestContext ctx, WalkBuilding building, WalkCell interior, WalkPortalRef portalRef)
= BuildLookInFixture(portalSide: 1);
var pview = new WalkPView();
var sink = new RecordingSink();
Assert.False(WalkBuildingPortals.DrawPortal(pview, building, portalRef, 1, ctx, sink));
Assert.Empty(sink.Punches);
Assert.Empty(sink.DrawCells);
}
[Fact]
public void Unloaded_destination_skips_the_punch_silently()
{
(TestContext ctx, WalkBuilding building, WalkCell interior, WalkPortalRef portalRef)
= BuildLookInFixture();
ctx.Cells.Remove(0x104); // destination not Visible
var pview = new WalkPView();
var sink = new RecordingSink();
Assert.False(WalkBuildingPortals.DrawPortal(pview, building, portalRef, 1, ctx, sink));
Assert.Empty(sink.Punches); // no fallback seal on the outdoor path
}
}