acdream/tests/AcDream.App.Tests/Rendering/Walk/RetailFrameWalkTests.cs
Erik c673f767e9 feat(rendering): port retail building degrade walk
Add exact retail building degrade selection, shared FPS/degrade ownership, complete-body gating, selected-shell submission, ordinary ladder mesh residency, frame-scoped retry rearm, Config controls, installed-DAT census, and lifecycle/allocation proofs.

Reviews: OpenAI retail pass 3 PASS; OpenAI production pass 5 PASS. Gates: Release 0W/0E; focused 285/285; hermetic 16811/16811; InstalledDat 469 pass, 10 documented fail, 1 documented skip; both manifests 30/30.
2026-09-04 22:29:18 +02:00

664 lines
28 KiB
C#

using System.Numerics;
using AcDream.App.Rendering.Walk;
namespace AcDream.App.Tests.Rendering.Walk;
public sealed class RetailFrameWalkTests
{
private sealed class Recorder : IWalkEventSink
{
public readonly List<WalkEvent> Events = new();
/// <summary>S3 chunk 3 (§9.3 T2): the SAME sequence <see cref="Signature"/>
/// reads plus <c>LC</c>/<c>SC</c> land-cell turns interleaved at the
/// exact point <see cref="RetailFrameWalk.DrawLandscape"/> fires
/// them — proves the terrain/building/object-turn ORDER, not just
/// each vocabulary in isolation.</summary>
public readonly List<string> Combined = new();
public int BuildingAlphaTurns;
public readonly List<WalkBuildingSelection> ShellSelections = new();
public void OnBuildingTurn(WalkBuilding building) => BuildingAlphaTurns++;
public void OnBuildingShellTurn(
WalkBuilding building,
WalkBuildingSelection selection) => ShellSelections.Add(selection);
public void Emit(in WalkEvent walkEvent)
{
Events.Add(walkEvent);
Combined.Add(walkEvent.Kind switch
{
WalkEventKind.Landscape => "LS",
WalkEventKind.Building => $"BLD:{walkEvent.CellId:x8}",
WalkEventKind.DrawInside => $"DI:{walkEvent.CellId:x8}",
WalkEventKind.DrawCells =>
$"DC:ov={walkEvent.OutsideViewCount}:{string.Join(',', walkEvent.Cells.Select(c => c.ToString("x8")))}",
_ => "?",
});
}
// S3 chunk 3: LC uses the SAME "(landblockId & 0xFFFF0000) |
// (cellIndex+1)" convention as the interface's own OnLandscapeCellTurn
// default (below) — comparable directly against SC for the same cell.
public void OnLandCellTurn(uint landblockId, int sideCellCount, int cellIndex)
=> Combined.Add(
$"LC:{(landblockId & 0xFFFF0000u) | (uint)(cellIndex + 1):x8}");
public void OnLandscapeCellTurn(uint cellId) => Combined.Add($"SC:{cellId:x8}");
/// <summary>S4-c2: retail <c>RenderDeviceD3D::DrawBlock</c>'s own
/// per-land-cell <c>FlushAlphaList(0.75f)</c> — recorded distinctly
/// from <see cref="OnLandscapeCellTurn(uint)"/>'s "SC" line so a test
/// can assert its exact position relative to both this cell's own SC
/// turn and the NEXT cell's LC turn.</summary>
public void OnSortCellExit(uint landblockId, int sideCellCount, int cellIndex)
=> Combined.Add(
$"SCX:{(landblockId & 0xFFFF0000u) | (uint)(cellIndex + 1):x8}");
public string Signature()
=> string.Join("|", Events.Select(e => e.Kind switch
{
WalkEventKind.Landscape => "LS",
WalkEventKind.Building => $"BLD:{e.CellId:x8}",
WalkEventKind.DrawInside => $"DI:{e.CellId:x8}",
WalkEventKind.DrawCells =>
$"DC:ov={e.OutsideViewCount}:{string.Join(',', e.Cells.Select(c => c.ToString("x8")))}",
_ => "?",
}));
}
private sealed class Caster : IWalkRayCaster
{
public Vector3 RayThrough(float screenX, float screenY)
=> new(screenX, screenY, 100f);
}
private class TestContext : IWalkFrameContext, IRetailFrameWalkContext
{
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;
}
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 virtual float ViewerDistanceTo(WalkBuilding building) => 0f;
public IWalkFrameContext CellContext => this;
// Permissive near plane: every column wholly inside.
// S3 chunk 3 (T2): settable, default unchanged — a permissive
// horizontal near plane every pre-existing test in this file still
// gets. T2 overrides it with a VERTICAL plane to carve a real
// out-of-view cell out of an admitted block via WalkVisibilityMath's
// OWN geometry (not a hand-set CellInView CheckBlocks would just
// overwrite).
public WalkPlane CyPlane { get; set; } = new(new Vector3(0, 0, 1), 0f);
public void SetActiveView(WalkPortalView views, int index) { }
public int ClipBuildingPolygon(
WalkBuilding building, WalkPolygon polygon, int side, Span<WalkScreenPoint> output)
=> 0; // no BSP-driven look-ins in these composition fixtures
}
private static WalkPolygon Quad(float z, bool facingViewer = true) => new()
{
Vertices =
[
new Vector3(-0.5f, -0.5f, z), new Vector3(0.5f, -0.5f, z),
new Vector3(0.5f, 0.5f, z), new Vector3(-0.5f, 0.5f, z),
],
Plane = new WalkPlane(new Vector3(0, 0, facingViewer ? 1f : -1f), facingViewer ? -z : z),
};
private static WalkLandscape Landscape3x3()
{
var landscape = new WalkLandscape
{
MidWidth = 3,
Blocks = new WalkLandBlock?[9],
ViewerBlockX = 1,
ViewerBlockY = 1,
ViewerCellX = 0,
ViewerCellY = 0,
};
for (int i = 0; i < 9; i++)
{
landscape.Blocks[i] = new WalkLandBlock { MaxZ = 10f, MinZ = 0f };
landscape.Blocks[i]!.EnsureCellArrays();
}
return landscape;
}
[Fact]
public void Outdoor_frame_emits_landscape_then_buildings_far_to_near()
{
var ctx = new TestContext();
WalkLandscape landscape = Landscape3x3();
// A building in ring-1 block (0,0), and two in the viewer block:
// one at the block's far corner, one at the viewer's closest cell.
var ringBuilding = new WalkBuilding { PositionCellId = 0xAAAA0001 };
var farBuilding = new WalkBuilding { PositionCellId = 0xBBBB0002 };
var nearBuilding = new WalkBuilding { PositionCellId = 0xCCCC0003 };
landscape.Blocks[0]!.CellBuildings[0] = ringBuilding; // block (0,0)
WalkLandBlock viewerBlock = landscape.Blocks[1 * 3 + 1]!;
viewerBlock.CellBuildings[7 * 8 + 7] = farBuilding; // far corner cell
viewerBlock.CellBuildings[0] = nearBuilding; // the closest cell (viewer at 0,0)
var walk = new RetailFrameWalk();
var recorder = new Recorder();
// ViewCount == 0 exercises the CY-only visibility arm deterministically.
walk.DrawLandscape(landscape, new WalkPortalView(), ctx, recorder);
Assert.Equal(
"LS|BLD:aaaa0001|BLD:bbbb0002|BLD:cccc0003",
recorder.Signature());
}
[Fact]
public void Degraded_building_still_emits_its_entry_event()
{
var ctx = new TestContext();
var walk = new RetailFrameWalk();
var recorder = new Recorder();
var building = new WalkBuilding { PositionCellId = 0xF518002E, GfxObjId = 0 };
walk.DrawBuilding(building, new WalkPortalView(), ctx, recorder);
Assert.Equal("BLD:f518002e", recorder.Signature());
Assert.Equal(0, recorder.BuildingAlphaTurns);
Assert.Empty(recorder.ShellSelections);
}
[Fact]
public void NullBspSelectedGfxStillRunsAlphaAndShellWithoutPortalWalk()
{
var building = new WalkBuilding
{
PositionCellId = 0xF518002Eu,
GfxObjId = 0x01000001u,
DrawingBsp = null,
};
var recorder = new Recorder();
new RetailFrameWalk().DrawBuilding(
building, new WalkPortalView(), new TestContext(), recorder);
Assert.Equal("BLD:f518002e", recorder.Signature());
Assert.Equal(1, recorder.BuildingAlphaTurns);
WalkBuildingSelection selected = Assert.Single(recorder.ShellSelections);
Assert.Equal(0x01000001u, selected.GfxObjId);
Assert.Null(selected.DrawingBsp);
}
[Fact]
public void SelectionUsesStrictThresholdAndFinalZeroIsCompleteBodyFailure()
{
var firstBsp = new WalkBspNode();
var building = new WalkBuilding
{
GfxObjId = 0x0100FFFFu,
DegradeLevels =
[
new(0x01000001u, 1u, 10f, 20f, 30f, firstBsp),
new(0u, 1u, 30f, 40f, 50f, null),
],
};
Assert.Equal(0x01000001u, building.Select(29.999f, 0f, 1f).GfxObjId);
WalkBuildingSelection equality = building.Select(30f, 0f, 1f);
Assert.Equal(0u, equality.GfxObjId);
Assert.Equal(1, equality.Level);
var recorder = new Recorder();
var context = new DistanceContext(80f);
new RetailFrameWalk().DrawBuilding(
building, new WalkPortalView(), context, recorder);
Assert.Equal("BLD:00000000", recorder.Signature());
Assert.Equal(0, recorder.BuildingAlphaTurns);
Assert.Empty(recorder.ShellSelections);
}
[Fact]
public void SelectionComparesStoredFloatEffectiveDistanceToWideThreshold()
{
const float multiplier = 0.0020020019728690386f;
const float effective = 24.04804801940918f;
Assert.Equal(0x41C06267u, BitConverter.SingleToUInt32Bits(effective));
var building = new WalkBuilding
{
DegradeLevels =
[
new(0x01000001u, 1u, 0f, 24f, 48f, null),
new(0x01000002u, 1u, 48f, 64f, 80f, null),
],
};
WalkBuildingSelection selected = building.Select(
effective, degradeDistance: 0f, degradeMultiplier: multiplier);
Assert.Equal(0, selected.Level);
Assert.Equal(0x01000001u, selected.GfxObjId);
}
[Fact]
public void DirectAndLadderSelectionsCarryExactIdBspLevelAndMode()
{
var directBsp = new WalkBspNode();
var direct = new WalkBuilding
{
GfxObjId = 0x01000010u,
DrawingBsp = directBsp,
};
Assert.Equal(
new WalkBuildingSelection(0x01000010u, directBsp, 0, 1u),
direct.Select(float.NaN, 50f, float.NaN));
var nearBsp = new WalkBspNode();
var middleBsp = new WalkBspNode();
var ladder = new WalkBuilding
{
GfxObjId = 0x0100FFFFu,
DegradeLevels =
[
new(0x01000011u, 3u, 10f, 20f, 30f, nearBsp),
new(0x01000012u, 5u, 30f, 40f, 50f, middleBsp),
new(0u, 7u, 50f, 60f, 70f, null),
],
};
Assert.Equal(
new WalkBuildingSelection(0x01000011u, nearBsp, 0, 3u),
ladder.Select(29f, 5f, 0.5f)); // effective 24 < positive threshold 25
Assert.Equal(
new WalkBuildingSelection(0x01000011u, nearBsp, 0, 3u),
ladder.Select(-19f, 5f, -0.5f)); // effective 14 < negative threshold 15
Assert.Equal(
new WalkBuildingSelection(0x01000012u, middleBsp, 1, 5u),
ladder.Select(30f, 5f, 0.5f)); // equality advances
Assert.Equal(2, ladder.Select(float.PositiveInfinity, 5f, 0f).Level);
Assert.Equal(2, ladder.Select(float.NegativeInfinity, 5f, 0f).Level);
Assert.Equal(2, ladder.Select(float.NaN, 5f, 0f).Level);
}
[Fact]
public void DisableAndForcedLevelBranchesPreserveRetailPrecedenceAndClamp()
{
var firstBsp = new WalkBspNode();
var building = new WalkBuilding
{
DegradeLevels =
[
new(0x01000021u, 11u, 1f, 2f, 3f, firstBsp),
new(0x01000022u, 12u, 3f, 4f, 5f, null),
new(0u, 13u, 5f, 6f, 7f, null),
],
};
Assert.Equal(
new WalkBuildingSelection(0x01000021u, firstBsp, 0, 11u),
building.Select(float.PositiveInfinity, 50f, 1f,
degradesDisabled: true, forcedLevel: 2));
Assert.Equal(1, building.Select(0f, 50f, 0f, forcedLevel: 1).Level);
WalkBuildingSelection clamped = building.Select(0f, 50f, 0f, forcedLevel: 99);
Assert.Equal(2, clamped.Level);
Assert.Equal(13u, clamped.Mode);
Assert.Equal(0u, clamped.GfxObjId);
}
private sealed class DistanceContext(float distance) : TestContext
{
public override float ViewerDistanceTo(WalkBuilding building) => distance;
}
// ── T2 (S3 chunk 3 §9.3): the per-cell interleave — DrawLandCell (LC)
// fires BEFORE its own cell's building/object turn, cells draw
// far-to-near, an out-of-view cell under alwaysDrawObjects keeps its
// sort turn (SC) but gets NO LC, and no whole-stage terrain event
// exists any more (superseded — S3 §9.1 R4). Real CalcDrawOrder/
// CheckBlocks run (not hand-set and bypassed): a VERTICAL CyPlane
// (N=(1,0,0)) at X-threshold 30 carves an honest "one column
// out-of-view" result out of the viewer's own 8-side block via
// WalkVisibilityMath's OWN corner-vs-plane test — LandCellCheck's
// n!=8 branch marks EVERY cell PartiallyInside unconditionally, so an
// out-of-view CELL (as opposed to a whole out-of-view BLOCK, which
// DrawLandscape's own block-level gate skips entirely, LC and SC
// alike) is only reachable at full n==8 resolution — hence the near
// block's own SC count is unavoidably 64 (AlwaysDrawObjects fires for
// every cell of an admitted block regardless of individual
// visibility), so this test asserts STRUCTURALLY (membership + local
// order) rather than as one 64-cell literal sequence; T1 separately
// pins the exact draw ORDER via LandWalkOrder + CalcDrawOrder against
// the real retail transcripts. ────────────────────────────────────────
[Fact]
public void DrawLandscape_InterleavesLandCellTurnsWithBuildingAndObjectTurns_FarToNear()
{
var ctx = new TestContext
{
// Vertical plane (Z-independent): GetPointLimit's third arm
// reduces to "d = x*1 + D < -eps => Outside" at (x, y, 0) — a
// pure world-X half-space test, the CY-only arm (ViewCount==0
// below) is the sole plane BlockCheck consults.
CyPlane = new WalkPlane(new Vector3(1, 0, 0), -30f),
};
var landscape = new WalkLandscape
{
MidWidth = 2,
Blocks = new WalkLandBlock?[4],
ViewerBlockX = 0,
ViewerBlockY = 0,
ViewerCellX = 0,
ViewerCellY = 0,
};
// The viewer's own block (grid slot 0 = (0,0)): world X/Y in
// [0, 192). Threshold 30 excludes the cx=0 column (X in [0,24], both
// corners < 30) and admits every other column (cx=1's west corner
// straddles at X=24 -> PartiallyInside; cx>=2 is fully >= 30).
var nearBlock = new WalkLandBlock
{
LandblockId = 0x11110000u, SideCellCount = 8, MaxZ = 10f, MinZ = 0f,
};
nearBlock.EnsureCellArrays();
// A building at an ordinary in-view cell (cx=3, cy=0), away from
// both the excluded column and the closest/farthest cells.
var building = new WalkBuilding { PositionCellId = 0x11110019u, GfxObjId = 0 };
nearBlock.CellBuildings[3 * 8 + 0] = building;
// The far block (grid slot 3 = (1,1), the diagonal corner — drawn
// FIRST since GetBlockOrder puts ring-1 slots ahead of the viewer's
// own and DrawLandscape walks that list BACKWARDS). World X/Y in
// [192, 384) sits comfortably east of the threshold -> the block
// itself is EntirelyInside; its one LOD-1 cell still classifies
// PartiallyInside (LandCellCheck's n!=8 branch marks every cell of
// a coarse block that way unconditionally) — either way, != Outside,
// so it still gets an LC turn, which is all this test needs.
var farBlock = new WalkLandBlock
{
LandblockId = 0x22220000u, SideCellCount = 1, MaxZ = 10f, MinZ = 0f,
};
farBlock.EnsureCellArrays();
landscape.Blocks[0] = nearBlock;
landscape.Blocks[3] = farBlock;
var walk = new RetailFrameWalk(); // AlwaysDrawObjects defaults true (retail default).
var recorder = new Recorder();
// ViewCount == 0 exercises the CY-only visibility arm deterministically
// (matches Outdoor_frame_emits_landscape_then_buildings_far_to_near's
// own convention) — the CyPlane above is the ONLY plane consulted.
walk.DrawLandscape(landscape, new WalkPortalView(), ctx, recorder);
Assert.Equal("LS", recorder.Combined[0]);
// S3 chunk 3 fix round 1 (F6): the "no TERRAIN event" assertion this
// line used to make was vacuous — this Recorder's own vocabulary
// (Combined) never had a "TERRAIN"-prefixed entry to begin with, so
// the assertion could never fail. WalkFrameEventKind has no
// TerrainSlice case any more (chunk 3 deleted it outright), and the
// driver-level pins in WalkFrameDriverTests (F4: the outdoor-root,
// interior-root, and cross-landblock-batching RunFrame tests) are
// what actually prove no whole-stage terrain draw survives.
const uint farCellId = 0x22220001u;
int farLcIndex = recorder.Combined.IndexOf($"LC:{farCellId:x8}");
int farScIndex = recorder.Combined.IndexOf($"SC:{farCellId:x8}");
Assert.True(farLcIndex >= 0 && farScIndex > farLcIndex, "far block: LC before SC");
var outsideIds = new List<uint>();
var insideIds = new List<uint>();
for (int cx = 0; cx < 8; cx++)
for (int cy = 0; cy < 8; cy++)
{
uint cellId = 0x11110000u | (uint)(cx * 8 + cy + 1);
(cx == 0 ? outsideIds : insideIds).Add(cellId);
}
// Far block draws entirely before ANY near-block content — the
// far-to-near block order.
int nearFirstIndex = insideIds.Concat(outsideIds)
.Select(id => recorder.Combined.IndexOf($"SC:{id:x8}"))
.Where(i => i >= 0)
.Min();
Assert.True(farScIndex < nearFirstIndex, "far block must draw before the near block");
// The excluded column: SC fires (AlwaysDrawObjects), LC never does.
foreach (uint cellId in outsideIds)
{
Assert.Contains($"SC:{cellId:x8}", recorder.Combined);
Assert.DoesNotContain($"LC:{cellId:x8}", recorder.Combined);
}
// Every other near-block cell: LC before SC (with the one
// building's BLD sitting strictly between them for its own cell).
Assert.Equal(56, insideIds.Count);
foreach (uint cellId in insideIds)
{
int lc = recorder.Combined.IndexOf($"LC:{cellId:x8}");
int sc = recorder.Combined.IndexOf($"SC:{cellId:x8}");
Assert.True(lc >= 0, $"cell 0x{cellId:x8} must have an LC turn");
Assert.True(sc > lc, $"cell 0x{cellId:x8}: LC must precede SC");
}
const uint buildingCellId = 0x11110019u;
int bldIndex = recorder.Combined.IndexOf($"BLD:{buildingCellId:x8}");
int buildingLc = recorder.Combined.IndexOf($"LC:{buildingCellId:x8}");
int buildingSc = recorder.Combined.IndexOf($"SC:{buildingCellId:x8}");
Assert.True(bldIndex >= 0, "the building's own cell must emit BLD");
Assert.True(buildingLc < bldIndex && bldIndex < buildingSc, "LC, then BLD, then SC");
}
/// <summary>S4-c2: <c>RenderDeviceD3D::DrawBlock</c>'s own per-land-cell
/// <c>FlushAlphaList(0.75f)</c> call — pinned at the exact position spec
/// §7 site 2 requires: strictly AFTER that cell's own object-list turn
/// (<c>OnLandscapeCellTurn</c>/"SC") and strictly BEFORE the next
/// visited cell's terrain turn (<c>OnLandCellTurn</c>/"LC"). Reuses
/// <see cref="DrawLandscape_InterleavesLandCellTurnsWithBuildingAndObjectTurns_FarToNear"/>'s
/// exact two-block far/near setup so the "next cell" crossing is a real
/// cross-BLOCK boundary, not a same-block artifact. Mutation check:
/// emitting <c>OnSortCellExit</c> BEFORE <c>OnLandscapeCellTurn</c> (the
/// wrong side of the object turn) would place SCX before SC for the far
/// cell — the first assertion fails; emitting it once per BLOCK instead
/// of once per CELL would produce only one SCX total instead of one per
/// near-block cell — the count assertion fails.</summary>
[Fact]
public void DrawLandscape_EmitsSortCellExitAfterEachCellsObjectTurnAndBeforeTheNextCellsLandTurn()
{
var ctx = new TestContext
{
CyPlane = new WalkPlane(new Vector3(1, 0, 0), -30f),
};
var landscape = new WalkLandscape
{
MidWidth = 2,
Blocks = new WalkLandBlock?[4],
ViewerBlockX = 0,
ViewerBlockY = 0,
ViewerCellX = 0,
ViewerCellY = 0,
};
var nearBlock = new WalkLandBlock
{
LandblockId = 0x11110000u, SideCellCount = 8, MaxZ = 10f, MinZ = 0f,
};
nearBlock.EnsureCellArrays();
var farBlock = new WalkLandBlock
{
LandblockId = 0x22220000u, SideCellCount = 1, MaxZ = 10f, MinZ = 0f,
};
farBlock.EnsureCellArrays();
landscape.Blocks[0] = nearBlock;
landscape.Blocks[3] = farBlock;
var walk = new RetailFrameWalk();
var recorder = new Recorder();
walk.DrawLandscape(landscape, new WalkPortalView(), ctx, recorder);
// The far block's single cell: SCX sits strictly between its own SC
// turn and the FIRST cell of the near block (the very next LC the
// walk visits, per the far-to-near block order this landscape's
// sibling test already proves).
const uint farCellId = 0x22220001u;
int farSc = recorder.Combined.IndexOf($"SC:{farCellId:x8}");
int farScx = recorder.Combined.IndexOf($"SCX:{farCellId:x8}");
Assert.True(farSc >= 0 && farScx == farSc + 1, "SCX must immediately follow its own cell's SC");
int firstNearLc = -1;
for (int i = 0; i < recorder.Combined.Count; i++)
{
if (recorder.Combined[i].StartsWith("LC:11110", StringComparison.Ordinal))
{
firstNearLc = i;
break;
}
}
Assert.True(firstNearLc > farScx, "the far cell's SCX must precede the near block's first LC");
// Every near-block cell (visited or not — SC/SCX fire under
// AlwaysDrawObjects regardless of LC) gets exactly one SCX, each
// immediately after that same cell's own SC.
for (int cx = 0; cx < 8; cx++)
for (int cy = 0; cy < 8; cy++)
{
uint cellId = 0x11110000u | (uint)(cx * 8 + cy + 1);
int sc = recorder.Combined.IndexOf($"SC:{cellId:x8}");
int scx = recorder.Combined.IndexOf($"SCX:{cellId:x8}");
Assert.True(sc >= 0, $"cell 0x{cellId:x8} must have an SC turn");
Assert.Equal(sc + 1, scx);
}
// Total SCX count equals the total cell count (1 far + 64 near) —
// one per cell, never batched per block.
Assert.Equal(65, recorder.Combined.Count(e => e.StartsWith("SCX:", StringComparison.Ordinal)));
}
[Fact]
public void Interior_frame_without_exit_views_skips_the_landscape()
{
var ctx = new TestContext();
// One facing portal only: the flood stays in the cell, no exit view.
var cell = new WalkCell
{
CellId = 0xA9B40178,
Portals = [new WalkCellPortal
{
OtherCellId = 0xA9B40179, PolygonIndex = 0, PortalSide = 1, OtherPortalId = 0,
}],
PortalPolygons = [Quad(-2f)],
};
ctx.Cells[cell.CellId] = cell;
var walk = new RetailFrameWalk();
var recorder = new Recorder();
walk.WalkFrame(cell.CellId, cell, Landscape3x3(), ctx, recorder);
Assert.Equal("DI:a9b40178|DC:ov=0:a9b40178", recorder.Signature());
}
[Fact]
public void Interior_frame_with_an_exit_view_draws_the_landscape_through_it()
{
var ctx = new TestContext();
var cell = new WalkCell
{
CellId = 0xA9B40150,
Portals = [new WalkCellPortal
{
OtherCellId = 0xFFFFFFFF, PolygonIndex = 0, PortalSide = 0, OtherPortalId = -1,
}],
PortalPolygons = [Quad(-2f)],
};
ctx.Cells[cell.CellId] = cell;
var walk = new RetailFrameWalk();
var recorder = new Recorder();
var landscape = new WalkLandscape
{
MidWidth = 1,
Blocks = new WalkLandBlock?[1],
ViewerBlockX = 0,
ViewerBlockY = 0,
};
walk.WalkFrame(cell.CellId, cell, landscape, ctx, recorder);
Assert.Equal("DI:a9b40150|DC:ov=1:a9b40150|LS", recorder.Signature());
}
// ── T4 / R1 (S3 chunk 2): RenderDeviceD3D::Init @0x0059efb0 constructs
// TWO PViews — indoor_pview = PView(…, 1) [draw_landscape=true] and
// outdoor_pview = PView(…, 0) [draw_landscape=false]. A building
// look-in's own DrawCells always runs through the OUTDOOR pview
// (RetailFrameWalk.DrawBuilding passes _outdoorPView into
// WalkBuildingPortals.DrawPortal), so it can never raise an outside
// view regardless of what portals the reached cell has — the mechanism
// itself (draw_landscape gating outside_view population) is pinned at
// the WalkPView level by
// WalkPViewFloodTests.Exit_portal_raises_the_outside_view_only_when_landscape_is_drawn;
// this test pins the ASSEMBLY fact that RetailFrameWalk wires the two
// PView instances with the correct flags.
[Fact]
public void RetailFrameWalk_WiresTheOutdoorPViewWithDrawLandscapeFalse_AndTheInteriorPViewTrue()
{
var walk = new RetailFrameWalk();
Assert.False(walk.OutdoorPView.DrawLandscape);
Assert.True(walk.InteriorPView.DrawLandscape);
}
[Fact]
public void Outdoor_camera_cell_roots_the_landscape_walk()
{
var ctx = new TestContext();
var walk = new RetailFrameWalk();
var recorder = new Recorder();
// Low word < 0x100 = an outdoor landcell id.
walk.WalkFrame(0xA9B40015, null, Landscape3x3(), ctx, recorder);
Assert.StartsWith("LS", recorder.Signature());
}
[Fact]
public void View_state_unwinds_completely_after_an_interior_frame()
{
var ctx = new TestContext();
var stabCell = new WalkCell { CellId = 0xA9B40151 };
var cell = new WalkCell
{
CellId = 0xA9B40150,
StabList = [0xA9B40151u],
Portals = [new WalkCellPortal
{
OtherCellId = 0xA9B40151, PolygonIndex = 0, PortalSide = 0, OtherPortalId = 0,
}],
PortalPolygons = [Quad(-2f)],
};
stabCell.Portals = [new WalkCellPortal
{
OtherCellId = 0xA9B40150, PolygonIndex = 0, PortalSide = 1, OtherPortalId = 0,
}];
stabCell.PortalPolygons = [Quad(-2f)];
ctx.Cells[cell.CellId] = cell;
ctx.Cells[stabCell.CellId] = stabCell;
var walk = new RetailFrameWalk();
walk.WalkFrame(cell.CellId, cell, Landscape3x3(), ctx, new Recorder());
Assert.Equal(0, cell.NumView);
Assert.Equal(0, stabCell.NumView);
}
}