acdream/tests/AcDream.App.Tests/Rendering/Walk/WalkPViewFloodTests.cs
Erik 161ffaf4e9 feat(render) Campaign FW1: port the PView interior flood
WalkPView transcribes ConstructView @0x005a57b0, InitCell @0x005a4b70,
InsCellTodoList @0x005a4f50 (nearest-first pop), ClipPortals @0x005a5520,
OtherPortalClip @0x005a5400 (inverted ==0 sidedness), AddViewToPortals
@0x005a52d0 (update_count watermark, duplicate-reach arm), AddToCell
@0x005a4d90, SetOtherSeen @0x005a4e30, FixCellList/AdjustCellPlace/
AdjustDrawList/AdjustCellView @0x005a5250/0x005a5010/0x005a4e90/
0x005a5770, plus set_view and PView::GetClip @0x005a4320 over the
WalkScreenClip chain. Resolves and documents the portal-flag convention
(inflag=1 = the portal FACES the viewer and feeds max_indist; inflag=0 +
armed seen = an opening — the same side condition building look-ins
require), which the PDB names obscure. WalkWorld supplies the cell model
and frame-context seam. Six synthetic-world flood tests pass on first
run: traversal, facing rejection with the exact distance key, exit-view
raising, entry-portal no-ping-pong, chain ordering, unloaded skips.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 09:58:28 +02:00

192 lines
7.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Numerics;
using AcDream.App.Rendering.Walk;
namespace AcDream.App.Tests.Rendering.Walk;
public sealed class WalkPViewFloodTests
{
private sealed class TestContext : IWalkFrameContext
{
private sealed class Caster : IWalkRayCaster
{
public Vector3 RayThrough(float screenX, float screenY)
=> new(screenX, screenY, 100f);
}
public readonly Dictionary<uint, WalkCell> Cells = new();
private readonly Matrix4x4 _viewProj;
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;
}
// All test cells sit at identity transforms: cell-local == world.
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;
}
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 through the quad: for z=-2 facing +z, N=(0,0,1), D=2 (eye at
// origin sits on the POSITIVE side: d = +2).
Plane = new WalkPlane(new Vector3(0, 0, facingViewer ? 1f : -1f), facingViewer ? -z : z),
};
private static WalkCell Cell(
TestContext ctx, uint id, params (WalkCellPortal Portal, WalkPolygon Polygon)[] portals)
{
var cell = new WalkCell
{
CellId = id,
Portals = portals.Select(p => p.Portal).ToArray(),
PortalPolygons = portals.Select(p => p.Polygon).ToArray(),
};
cell.PushView(); // add_views/stab-list stand-in: one pushed slot
ctx.Cells[id] = cell;
return cell;
}
private static WalkPView SeedAndFlood(TestContext ctx, WalkCell seed)
{
var pview = new WalkPView();
WalkCopyView.AppendFullViewportQuad(
seed.TopView, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
pview.ConstructView(seed, 0xFFFF, ctx);
return pview;
}
[Fact]
public void Flood_traverses_a_look_through_portal_into_the_neighbor()
{
var ctx = new TestContext();
// Eye on the positive side of the portal plane (d=+2 → side 0);
// PortalSide=0 → side == PortalSide → an OPENING (look-through).
WalkCell seed = Cell(ctx, 0x100,
(new WalkCellPortal { OtherCellId = 0x101, PolygonIndex = 0, PortalSide = 0, OtherPortalId = 0 },
Quad(-2f)));
WalkCell neighbor = Cell(ctx, 0x101,
(new WalkCellPortal { OtherCellId = 0x100, PolygonIndex = 0, PortalSide = 1, OtherPortalId = 0 },
Quad(-2f)));
WalkPView pview = SeedAndFlood(ctx, seed);
Assert.Equal(new[] { 0x100u, 0x101u }, pview.CellDrawList.Select(c => c.CellId));
Assert.Equal(1, neighbor.TopView.ViewCount); // the clipped view arrived
Assert.True(neighbor.TopView.CellViewDone); // and the neighbor was processed
}
[Fact]
public void Facing_portal_is_not_traversed_but_feeds_the_distance_key()
{
var ctx = new TestContext();
// PortalSide=1 while the eye computes side 0 → the portal FACES the
// viewer (a visible surface, not an opening).
WalkCell seed = Cell(ctx, 0x100,
(new WalkCellPortal { OtherCellId = 0x101, PolygonIndex = 0, PortalSide = 1, OtherPortalId = 0 },
Quad(-2f)));
Cell(ctx, 0x101,
(new WalkCellPortal { OtherCellId = 0x100, PolygonIndex = 0, PortalSide = 0, OtherPortalId = 0 },
Quad(-2f)));
WalkPView pview = SeedAndFlood(ctx, seed);
Assert.Equal(new[] { 0x100u }, pview.CellDrawList.Select(c => c.CellId));
// max_indist = squared distance to the farthest facing-portal vertex:
// (±0.5, ±0.5, 2) from the origin → 0.25 + 0.25 + 4.
Assert.Equal(4.5f, seed.TopView.MaxInDistSquared, 3);
}
[Fact]
public void Exit_portal_raises_the_outside_view_only_when_landscape_is_drawn()
{
var ctx = new TestContext();
WalkCell seed = Cell(ctx, 0x100,
(new WalkCellPortal { OtherCellId = 0xFFFFFFFF, PolygonIndex = 0, PortalSide = 0, OtherPortalId = -1 },
Quad(-2f)));
WalkPView pview = SeedAndFlood(ctx, seed);
Assert.Equal(1, pview.OutsideView.ViewCount);
// draw_landscape == 0 discards exit views entirely.
seed.PopView();
seed.PushView();
var noLandscape = new WalkPView { DrawLandscape = false };
WalkCopyView.AppendFullViewportQuad(
seed.TopView, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
noLandscape.ConstructView(seed, 0xFFFF, ctx);
Assert.Equal(0, noLandscape.OutsideView.ViewCount);
}
[Fact]
public void Flood_never_walks_back_through_the_entry_portal()
{
var ctx = new TestContext();
// Two cells looking into each other; both sides traversable by
// geometry. Without the entry-portal force, the flood would ping-pong.
WalkCell a = Cell(ctx, 0x100,
(new WalkCellPortal { OtherCellId = 0x101, PolygonIndex = 0, PortalSide = 0, OtherPortalId = 0 },
Quad(-2f)));
WalkCell b = Cell(ctx, 0x101,
(new WalkCellPortal { OtherCellId = 0x100, PolygonIndex = 0, PortalSide = 0, OtherPortalId = 0 },
Quad(-2f)));
WalkPView pview = SeedAndFlood(ctx, a);
Assert.Equal(2, pview.CellDrawList.Count);
Assert.Equal(2, pview.CellDrawList.Select(c => c.CellId).Distinct().Count());
}
[Fact]
public void Deeper_chain_floods_in_nearest_first_pop_order()
{
var ctx = new TestContext();
// seed → mid (portal at z=-2) → far (portal at z=-4): the draw list
// appends in pop order (nearest first), so the end-first draw walk
// is far-to-near.
WalkCell seed = Cell(ctx, 0x100,
(new WalkCellPortal { OtherCellId = 0x101, PolygonIndex = 0, PortalSide = 0, OtherPortalId = 0 },
Quad(-2f)));
WalkCell mid = Cell(ctx, 0x101,
(new WalkCellPortal { OtherCellId = 0x100, PolygonIndex = 0, PortalSide = 1, OtherPortalId = 0 },
Quad(-2f)),
(new WalkCellPortal { OtherCellId = 0x102, PolygonIndex = 1, PortalSide = 0, OtherPortalId = 0 },
Quad(-4f, half: 0.4f)));
WalkCell far = Cell(ctx, 0x102,
(new WalkCellPortal { OtherCellId = 0x101, PolygonIndex = 0, PortalSide = 1, OtherPortalId = 1 },
Quad(-4f, half: 0.4f)));
WalkPView pview = SeedAndFlood(ctx, seed);
Assert.Equal(
new[] { 0x100u, 0x101u, 0x102u },
pview.CellDrawList.Select(c => c.CellId));
Assert.Equal(1, far.TopView.ViewCount);
}
[Fact]
public void Unloaded_neighbor_is_silently_skipped()
{
var ctx = new TestContext();
WalkCell seed = Cell(ctx, 0x100,
(new WalkCellPortal { OtherCellId = 0x0DEAD, PolygonIndex = 0, PortalSide = 0, OtherPortalId = 0 },
Quad(-2f)));
WalkPView pview = SeedAndFlood(ctx, seed);
Assert.Equal(new[] { 0x100u }, pview.CellDrawList.Select(c => c.CellId));
}
}