acdream/tests/AcDream.App.Tests/Rendering/Walk/RetailFrameWalkTests.cs
Erik 651badc2b9 fix(render): S3 chunk 3 round 1 - slot key, deferred cross-block terrain batches, per-frame terrain diagnostic
Fixes the three-lens review blockers against 671eb3ad4 (S3 section 9.6 F1-F6).

F1 - slot key (blocking, retail). TerrainModernRenderer.DrawLandCells
normalizes every incoming landblockId to (id & 0xFFFF0000u) | 0xFFFFu
before the _idToSlot lookup: the walk hands 0xXXYY0000
(WalkLandBlock.LandblockId) but AddLandblock stores under the DAT id
0xXXYYFFFF (LandblockRenderPublisher.LandblockId) - every walk lookup
was missing and the walk path drew NO terrain. Unit-tested end-to-end
through a real RecordingGpuDevice-backed TerrainModernRenderer
(TerrainWalkSlotKeyNormalizationTests): AddLandblock(0xA9B4FFFF, ...)
is found by a 0xA9B40000 lookup, an unknown landblock is a silent
per-entry no-op, and a batch mixing a known and unknown entry submits
only the known one.

F2 - deferred cross-block batching (blocking, driver). Retail's
DrawSortCell always follows DrawLandCell (LC/SC strictly alternate,
never two LC in a row - S3 section 9 R1), so chunk 3's "merge
consecutive same-landblock LandCell events" rule never actually
merged anything; the driver review flagged batching as inert.
WalkFrameDriver.Replay now keeps ONE pending terrain batch across
landblocks ((landblockId, side, cellIndex) entries, cleared at
Replay's own start); a LandCell event only appends; every OTHER event
kind that will itself submit GPU work (StreamMark, Sky, CellShell,
PunchFan, AlphaBarrier, LandscapeFlush, ClearInteriorDepth,
ExitSeals) flushes the pending batch first; a StaticParticles/
CellParticles turn asks the new ParticleSystem.
HasRenderableEmittersInCell (an allocation-free sibling of
CopyRenderableEmittersInCell) and, when the cell has no renderable
emitter, submits nothing and does NOT flush either - the whole point
of the deferred rule. The end of Replay flushes the remainder. This
is order-preserving by construction: a flush always lands at the
exact point the unbatched draw would have, so GPU submission order -
and therefore pixels - is identical to the unbatched baseline; only
the number of small terrain draw calls shrinks.
TerrainModernRenderer.DrawLandCellRuns becomes DrawLandCells(
viewProjection, IReadOnlyList<(uint LandblockId, int SideCellCount,
int CellIndex)>) - one MultiDrawIndexedIndirect over every entry's
runs, unknown slots skipped per-entry. IWalkFrameLeafRenderer.
DrawLandCellBatch drops its separate landblockId parameter to match
(a batch can span several landblocks now) and gains
HasRenderableEmittersInCell.

Batch-count demonstration: driven through a real WalkFrameDriver
Replay (OnLandCellTurn_MergesAcrossLandblocksOverAnEmptyParticleTurn_
RealSubmissionsSplit), 4 LandCell turns across 3 distinct landblocks,
separated only by an empty particle turn, a real StreamMark, and a
building's alpha barrier, submit as exactly 3 DrawLandCellBatch calls
(2+1+1) instead of 4 - the empty particle turn's non-flush merges two
otherwise-separate cross-landblock entries. At production scale the
same mechanism is expected to cut the terrace-edge frame's ~578
individual DrawLandCell events (S3 section 9's captured transcript
count) to "tens" of submitted batches, per the contract's own
expectation: most terrain cells have no particle owner nearby, so the
strict LC/[empty-SC]/LC/[empty-SC]/... run collapses into one batch
per region bounded by real content (a building, a StreamMark-worthy
cell, or a genuine emitter) rather than per cell.

F3 - per-frame terrain diagnostic (blocking, build/test). The walk
leaf no longer brackets each batch with TerrainDrawDiagnosticsController
.Begin()/Complete() (a per-batch Stopwatch Restart/Stop pair that was
pushing one timing SAMPLE per batch, not per frame).
RetailPViewPassExecutor.DrawWalkLandCellBatch instead times its own
call with a raw Stopwatch.GetTimestamp() delta (no allocation) and
hands the ticks to the controller's new AccumulateWalkBatch;
RetailPViewRenderer.DrawWalkDrivenStatics calls the new
CompleteWalkTerrainFrame() exactly once, immediately after
driver.Replay finishes - "the end of the walk replay", where the
deleted whole-stage terrain leaf's own Begin()/Complete() bracket
used to close - which pushes ONE elapsed-time sample (even a
zero-batch frame pushes a zero sample: one sample per frame, not per
landscape turn) and publishes on the existing 5-second cadence.
TerrainRenderDiagnosticFacts gains a Draws field alongside
VisibleSlots (both were the same field before); TerrainModernRenderer
tracks its own per-frame WalkVisibleSlotCount/WalkDrawCount (a
HashSet<int>/int cleared in BeginFrame, populated by DrawLandCells),
and the diagnostics source reports those whenever the walk drew at
least one batch this frame, falling back to the non-walk Draw()
path's VisibleSlots otherwise (the two paths never both run in the
same frame). The [TERRAIN-DIAG] line's meaning (cpu_us per frame) is
unchanged, so the S3 section 9.5 before/after compare stays valid.

F4 - driver pins for the LandCell position (major). Three RunFrame-
level pins replace the deleted TERRAIN:0 pins: an outdoor-root
sequence (SKY, then one LANDCELL, driving RetailFrameWalk.
DrawLandscape directly with a one-view/zero-vertex WalkPortalView so
WalkLandscape.CheckBlocks' admission stays the same deterministic
"CY-only" test RetailFrameWalkTests already relies on, while still
satisfying WalkFrameDriver's real >=1-active-view fail-loud guard);
an interior-root test with one real exit view and one populated
block (SKY, LANDCELL, LFLUSH, SEALS, SHELL...) built on the existing
RunFrame_InteriorFloodWithExitView_... fixture; and the T4 batching
pin re-expressed for the F2 rule (OnLandCellTurn_
MergesAcrossLandblocksOverAnEmptyParticleTurn_RealSubmissionsSplit,
described above). The fake leaf's DrawLandCellBatch now logs
LANDCELL:<lb>:<side>:<idx>[,...] per batch and gains
HasRenderableEmittersInCell backed by an opt-out CellsWithoutEmitters
set (default true - has-emitters - so every pre-existing pin in the
file keeps its old unconditional-submission behavior unchanged).

F5 - no code change: the walk's in-view gate is unchanged; no
whole-block terrain re-added.

F6 - minor/notes: DrawLandCells' own comment now states the walk's
CheckBlocks/landcell_check admission is the sole terrain culling
authority (retail has no separate terrain frustum test); the
HandleLandscapeTurn comment's inverted claim is corrected (a FARTHER
building's punch survived because NEARER terrain was drawn BEFORE
it, not after - the interleave now draws it after, matching retail);
the "flat/directional-shadow paths" claim is corrected to the one
actual caller, WorldScenePassExecutor.DrawFlatTerrain (a directional-
shadow receiver selects its pipeline inside the SAME DrawRhi call,
not through a second caller); the cathedral order-trace token gains
the LOD side/index (":LC<lb>/<side>:<idx>"); T2's vacuous "no
TERRAIN event" assertion in RetailFrameWalkTests is replaced by a
comment pointing at the F4 driver-level pins; and the stale
"Confirmed OH5 defect" row in oh1-construction-landscape-contract.md
is retired with "FIXED by S3 chunk 3 (commit 671eb3ad4 + fix round
1)".

App hermetic lane: 6,795/6,795 (up from 671eb3ad4's 6,786 baseline -
net +9 tests: 3 F1 slot-key tests, 2 F4a/b driver RunFrame pins, 3
TerrainDrawDiagnosticsController walk-frame tests, plus the T4->F4c
rewrite and the RetailPViewPassExecutorTests split are net neutral).
InstalledDat lane: 241 passed, the same 3 accepted failures (2
pre-existing #383 layout fixture-drift tests, 1 TowerAscent
Status=KnownFailure) - unchanged from baseline. Core Vfx tests:
109/109 (108 baseline + 1 new HasRenderableEmittersInCell lifecycle
pin mirroring CopyRenderableEmittersInCell's own add/move/remove
test).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 09:57:34 +02:00

419 lines
18 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 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}");
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 sealed 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 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, HasGeometry = false };
walk.DrawBuilding(building, new WalkPortalView(), ctx, recorder);
Assert.Equal("BLD:f518002e", recorder.Signature());
}
// ── 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, HasGeometry = false };
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");
}
[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);
}
}