diff --git a/src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs b/src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs
new file mode 100644
index 00000000..2f26f277
--- /dev/null
+++ b/src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs
@@ -0,0 +1,194 @@
+using System.Numerics;
+
+namespace AcDream.App.Rendering.Walk;
+
+/// The composition context: the cell/building halves plus the
+/// frame-level state retail keeps in globals (the CY near plane from
+/// Render::update_viewpoint, and the active-view installation the
+/// building clip consumes).
+public interface IRetailFrameWalkContext : IWalkBuildingFrameContext
+{
+ WalkPlane CyPlane { get; }
+
+ /// Render::set_view at the frame level: install view
+ /// of as the active
+ /// clip context for subsequent building-polygon clips.
+ void SetActiveView(WalkPortalView views, int index);
+
+ float ViewportWidth { get; }
+ float ViewportHeight { get; }
+}
+
+///
+/// Campaign FW1 — the frame walk root, composing the ported machinery into
+/// retail's frame shapes (model:
+/// docs/research/2026-08-30-fw-walk-pseudocode.md §1, §3-§5; oracle:
+/// docs/research/2026-08-30-fw-walk-oracle/README.md):
+///
+///
+/// - Rooting (SmartBox::RenderNormalMode @0x00453aa0): the
+/// CAMERA cell's low word < 0x100 → outdoor root (default full-screen
+/// view + the landscape walk); otherwise → DrawInside.
+/// - Outdoor (LScape::draw @0x00506330): visibility per view,
+/// blocks far-to-near, per-block cells far-to-near, buildings at their
+/// cell's turn with the two-pass portal machinery (punches + look-ins on
+/// the OUTDOOR pview — the traces' pv=009d4b08 ov=0).
+/// - Interior (PView::DrawInside @0x005a5860 +
+/// DrawCells @0x005a4840): the flood on the INTERIOR pview
+/// (pv=009d4a80), then the landscape drawn THROUGH the exit views
+/// when any survive (ov>0).
+///
+///
+/// Two PView instances exist exactly as the live traces showed.
+///
+public sealed class RetailFrameWalk
+{
+ private readonly WalkPView _interiorPView = new();
+ private readonly WalkPView _outdoorPView = new();
+ private readonly WalkPortalView _defaultView = new();
+
+ public WalkPView InteriorPView => _interiorPView;
+ public WalkPView OutdoorPView => _outdoorPView;
+
+ /// The per-frame root (SmartBox::RenderNormalMode).
+ /// may be null only when the camera is
+ /// outdoors.
+ public void WalkFrame(
+ uint cameraCellId, WalkCell? cameraCell, WalkLandscape landscape,
+ IRetailFrameWalkContext ctx, IWalkEventSink sink)
+ {
+ if ((cameraCellId & 0xFFFF) < 0x100)
+ {
+ // Render::set_default_view @0x0054ef50: the full-screen quad view.
+ _defaultView.ResetForPush();
+ WalkCopyView.AppendFullViewportQuad(
+ _defaultView, ctx.Rays, ctx.WorldViewpoint,
+ ctx.ViewportWidth, ctx.ViewportHeight);
+ DrawLandscape(landscape, _defaultView, ctx, sink);
+ }
+ else
+ {
+ DrawInside(
+ cameraCell ?? throw new ArgumentNullException(nameof(cameraCell)),
+ landscape, ctx, sink);
+ }
+ }
+
+ /// PView::DrawInside + the event half of
+ /// DrawCells. The geometry/object passes emit no walk events.
+ public void DrawInside(
+ WalkCell cell, WalkLandscape landscape,
+ IRetailFrameWalkContext ctx, IWalkEventSink sink)
+ {
+ sink.Emit(WalkEvent.DrawInside(cell.CellId));
+ cell.PushView();
+ AddViews(cell.StabList, ctx);
+ WalkCopyView.AppendFullViewportQuad(
+ cell.TopView, ctx.Rays, ctx.WorldViewpoint,
+ ctx.ViewportWidth, ctx.ViewportHeight);
+ _interiorPView.ConstructView(cell, 0xFFFF, ctx.CellContext);
+
+ EmitDrawCells(_interiorPView, sink);
+ if (_interiorPView.OutsideView.ViewCount > 0)
+ DrawLandscape(landscape, _interiorPView.OutsideView, ctx, sink);
+
+ RemoveViews(cell.StabList, ctx);
+ cell.PopView();
+ }
+
+ /// LScape::draw: visibility per active view, then blocks
+ /// far-to-near, cells far-to-near, buildings at their cell's turn.
+ public void DrawLandscape(
+ WalkLandscape landscape, WalkPortalView activeViews,
+ IRetailFrameWalkContext ctx, IWalkEventSink sink)
+ {
+ sink.Emit(WalkEvent.Landscape());
+ landscape.CalcDrawOrder();
+ landscape.CheckBlocks(ctx.CyPlane, activeViews);
+
+ for (int i = landscape.BlockDrawCount - 1; i >= 0; i--)
+ {
+ WalkLandBlock? block = landscape.Blocks[landscape.BlockDrawList[i]];
+ if (block is null || block.InView == WalkBoundingType.Outside)
+ continue;
+ int cellCount = block.SideCellCount * block.SideCellCount;
+ for (int k = 0; k < cellCount; k++)
+ {
+ int cellIndex = block.DrawArray[k];
+ // DrawSortCell runs for in-view cells (alwaysDrawObjects
+ // default 0); terrain (DrawLandCell) emits no walk event.
+ if (block.CellInView[cellIndex] == WalkBoundingType.Outside)
+ continue;
+ if (block.CellBuildings[cellIndex] is WalkBuilding building)
+ DrawBuilding(building, activeViews, ctx, sink);
+ }
+ }
+ }
+
+ /// RenderDeviceD3D::DrawBuilding @0x0059f2a0 at the walk
+ /// level: the BLD event fires at ENTRY (before the degrade check); the
+ /// portal pass walks the drawing BSP twice per active view (punch, then
+ /// look-in + DrawCells on the outdoor pview); the shell pass emits no
+ /// walk event.
+ public void DrawBuilding(
+ WalkBuilding building, WalkPortalView activeViews,
+ IRetailFrameWalkContext ctx, IWalkEventSink sink)
+ {
+ sink.Emit(WalkEvent.Building(building.PositionCellId));
+ if (!building.HasGeometry) return;
+
+ int viewCount = Math.Max(activeViews.ViewCount, 0);
+ var passSink = new PortalPassSink(sink);
+ Vector3 viewpoint = ctx.ViewpointInBuilding(building);
+ for (int v = 0; v < viewCount; v++)
+ {
+ ctx.SetActiveView(activeViews, v);
+ WalkBuildingPortals.BuildDrawPortalsOnly(
+ building.DrawingBsp, 1, viewpoint,
+ (portalRef, pass) => WalkBuildingPortals.DrawPortal(
+ _outdoorPView, building, portalRef, pass, ctx, passSink));
+ WalkBuildingPortals.BuildDrawPortalsOnly(
+ building.DrawingBsp, 2, viewpoint,
+ (portalRef, pass) => WalkBuildingPortals.DrawPortal(
+ _outdoorPView, building, portalRef, pass, ctx, passSink));
+ }
+ }
+
+ private void EmitDrawCells(WalkPView pview, IWalkEventSink sink)
+ {
+ uint[] cells = new uint[pview.CellDrawList.Count];
+ for (int i = 0; i < cells.Length; i++)
+ cells[i] = pview.CellDrawList[i].CellId;
+ sink.Emit(WalkEvent.DrawCells(pview.OutsideView.ViewCount, cells));
+ }
+
+ private void AddViews(uint[] stabList, IRetailFrameWalkContext ctx)
+ {
+ foreach (uint id in stabList)
+ ctx.GetVisible(id)?.PushView();
+ }
+
+ private void RemoveViews(uint[] stabList, IRetailFrameWalkContext ctx)
+ {
+ foreach (uint id in stabList)
+ ctx.GetVisible(id)?.PopView();
+ }
+
+ private sealed class PortalPassSink(IWalkEventSink sink)
+ : WalkBuildingPortals.IWalkPortalPassSink
+ {
+ public void OnPunch(WalkPolygon polygon)
+ {
+ // The far-Z punch is a depth-only GPU submission (FW2's ordered
+ // stream); the oracle traces do not log it.
+ }
+
+ public void OnDrawCells(WalkPView pview)
+ {
+ uint[] cells = new uint[pview.CellDrawList.Count];
+ for (int i = 0; i < cells.Length; i++)
+ cells[i] = pview.CellDrawList[i].CellId;
+ sink.Emit(WalkEvent.DrawCells(pview.OutsideView.ViewCount, cells));
+ }
+ }
+}
diff --git a/src/AcDream.App/Rendering/Walk/WalkLandscape.cs b/src/AcDream.App/Rendering/Walk/WalkLandscape.cs
new file mode 100644
index 00000000..01818a63
--- /dev/null
+++ b/src/AcDream.App/Rendering/Walk/WalkLandscape.cs
@@ -0,0 +1,220 @@
+namespace AcDream.App.Rendering.Walk;
+
+/// One landblock as the walk sees it (retail CLandBlock):
+/// static shape plus the per-frame visibility the landscape pass writes.
+public sealed class WalkLandBlock
+{
+ public int SideCellCount = 8;
+ public float MaxZ;
+ public float MinZ;
+
+ /// Per cell index (x * side + y): the building the cell's
+ /// CSortCell owns, if any.
+ public WalkBuilding?[] CellBuildings = [];
+
+ // ---- per-frame visibility (draw_check_blocks / landcell_check) ----
+ public WalkBoundingType InView;
+ public WalkBoundingType[] CellInView = [];
+
+ // ---- per-block cell draw order cache (calc_sq_draw_order) ----
+ public int[] DrawArray = [];
+ public int ClosestX = -1;
+ public int ClosestY = -1;
+
+ public void EnsureCellArrays()
+ {
+ int n = SideCellCount * SideCellCount;
+ if (CellInView.Length < n) CellInView = new WalkBoundingType[n];
+ if (DrawArray.Length < n) DrawArray = new int[n];
+ if (CellBuildings.Length < n) CellBuildings = new WalkBuilding?[n];
+ }
+}
+
+/// The viewer-centered block grid (retail LScape): mid_width
+/// × mid_width blocks (retail 11×11), the near-to-far block draw list, and
+/// the viewer's grid offsets (calc_draw_order).
+public sealed class WalkLandscape
+{
+ public const float BlockLength = 192f; // 24 m cell × 8 (BN elided this to '0f')
+ public const float CellLength = 24f;
+
+ public int MidWidth = 11;
+ public WalkLandBlock?[] Blocks = []; // [x * MidWidth + y]
+ public int ViewerBlockX;
+ public int ViewerBlockY;
+ public int ViewerCellX; // viewer cell & 7 per axis (SqCoord)
+ public int ViewerCellY;
+
+ public int[] BlockDrawList = [];
+ public int BlockDrawCount;
+
+ public WalkLandBlock? BlockAt(int gridX, int gridY)
+ => gridX >= 0 && gridX < MidWidth && gridY >= 0 && gridY < MidWidth
+ ? Blocks[gridX * MidWidth + gridY]
+ : null;
+
+ /// LScape::get_block_order + per-block
+ /// calc_draw_order: rebuild the near-to-far block list and each
+ /// block's far-to-near cell order for the current viewer placement.
+ public void CalcDrawOrder()
+ {
+ if (BlockDrawList.Length < MidWidth * MidWidth)
+ BlockDrawList = new int[MidWidth * MidWidth];
+ BlockDrawCount = LandWalkOrder.GetBlockOrder(
+ ViewerBlockX, ViewerBlockY, MidWidth, BlockDrawList);
+
+ for (int x = 0; x < MidWidth; x++)
+ {
+ for (int y = 0; y < MidWidth; y++)
+ {
+ WalkLandBlock? block = Blocks[x * MidWidth + y];
+ if (block is null) continue;
+ block.EnsureCellArrays();
+ LandDirection dir = LandWalkOrder.GetDirection(
+ x - ViewerBlockX, y - ViewerBlockY);
+ (int cx, int cy) = LandWalkOrder.ClosestCell(
+ dir, ViewerCellX, ViewerCellY, block.SideCellCount);
+ // Retail's early-out: same closest cell keeps the stale order
+ // (the dir field is NOT compared — keep the quirk).
+ if (cx == block.ClosestX && cy == block.ClosestY) continue;
+ block.ClosestX = cx;
+ block.ClosestY = cy;
+ LandWalkOrder.FillCellOrderFarToNear(
+ cx, cy, block.SideCellCount,
+ block.DrawArray.AsSpan(0, block.SideCellCount * block.SideCellCount));
+ }
+ }
+ }
+
+ ///
+ /// LScape::draw_check_blocks @0x00505f80 + landcell_check
+ /// @0x005050a0: clear all visibility, then for EACH view of the active
+ /// portal view union block/cell visibility from clip-height interval
+ /// grids at 192 m block / 24 m cell pitch in viewer-block-relative
+ /// coordinates. A block/cell marked by an earlier view is never
+ /// downgraded (blocks are only written non-Outside; cells are skipped
+ /// once in view).
+ ///
+ public void CheckBlocks(in WalkPlane cyPlane, WalkPortalView activeViews)
+ {
+ foreach (WalkLandBlock? block in Blocks)
+ {
+ if (block is null) continue;
+ block.EnsureCellArrays();
+ block.InView = WalkBoundingType.Outside;
+ Array.Clear(block.CellInView, 0, block.SideCellCount * block.SideCellCount);
+ }
+
+ int viewCount = activeViews.ViewCount;
+ Span boundsScratch = stackalloc float[32];
+ // Corner interval grids: 2 rolling rows of (MidWidth+1) corners,
+ // each corner a plane-classification vector.
+ int cornerRow = MidWidth + 1;
+ var intervals = new float[2 * cornerRow][];
+ for (int i = 0; i < intervals.Length; i++) intervals[i] = new float[32];
+
+ int v = 0;
+ while (true)
+ {
+ WalkPlane[] edgePlanes;
+ int edgeCount;
+ bool last;
+ if (viewCount == 0)
+ {
+ edgePlanes = [];
+ edgeCount = 0;
+ last = true;
+ }
+ else
+ {
+ WalkViewPoly poly = activeViews.View.Polys[v];
+ edgePlanes = new WalkPlane[poly.VertexCount];
+ for (int k = 0; k < poly.VertexCount; k++)
+ edgePlanes[k] = activeViews.View.Vertices[poly.VertexIndex + k].Plane;
+ edgeCount = poly.VertexCount;
+ v++;
+ last = v == viewCount;
+ }
+
+ // Seed the west column (grid x = 0) into parity row 0.
+ for (int j = 0; j <= MidWidth; j++)
+ WalkVisibilityMath.FillClipHeights(
+ (0 - ViewerBlockX) * BlockLength,
+ (j - ViewerBlockY) * BlockLength,
+ cyPlane, edgePlanes, intervals[j]);
+ for (int bx = 0; bx < MidWidth; bx++)
+ {
+ int westRow = (bx & 1) * cornerRow;
+ int eastRow = ((bx - 1) & 1) * cornerRow;
+ for (int j = 0; j <= MidWidth; j++)
+ WalkVisibilityMath.FillClipHeights(
+ (bx + 1 - ViewerBlockX) * BlockLength,
+ (j - ViewerBlockY) * BlockLength,
+ cyPlane, edgePlanes, intervals[eastRow + j]);
+ for (int by = 0; by < MidWidth; by++)
+ {
+ WalkLandBlock? block = Blocks[bx * MidWidth + by];
+ if (block is null) continue;
+ WalkBoundingType bt = WalkVisibilityMath.BlockCheck(
+ intervals[westRow + by], intervals[westRow + by + 1],
+ intervals[eastRow + by], intervals[eastRow + by + 1],
+ edgeCount, block.MaxZ, block.MinZ);
+ if (bt != WalkBoundingType.Outside)
+ {
+ block.InView = bt;
+ LandCellCheck(block, bx, by, cyPlane, edgePlanes);
+ }
+ }
+ }
+ if (last) return;
+ }
+ }
+
+ // landcell_check @0x005050a0: non-8×8 far blocks mark all cells PARTIAL;
+ // ENTIRELY_INSIDE blocks mark all cells 2; else per-cell corner grids at
+ // 24 m pitch, skipping cells already in view (union across views).
+ private void LandCellCheck(
+ WalkLandBlock block, int bx, int by,
+ in WalkPlane cyPlane, WalkPlane[] edgePlanes)
+ {
+ int n = block.SideCellCount;
+ if (n != 8)
+ {
+ for (int i = 0; i < n * n; i++)
+ block.CellInView[i] = WalkBoundingType.PartiallyInside;
+ return;
+ }
+ if (block.InView == WalkBoundingType.EntirelyInside)
+ {
+ for (int i = 0; i < n * n; i++)
+ block.CellInView[i] = WalkBoundingType.EntirelyInside;
+ return;
+ }
+ float x0 = (bx - ViewerBlockX) * BlockLength;
+ float y0 = (by - ViewerBlockY) * BlockLength;
+ int cornerRow = n + 1;
+ var grid = new float[2 * cornerRow][];
+ for (int i = 0; i < grid.Length; i++) grid[i] = new float[32];
+ for (int j = 0; j <= n; j++)
+ WalkVisibilityMath.FillClipHeights(
+ x0, j * CellLength + y0, cyPlane, edgePlanes, grid[j]);
+ for (int cx = 0; cx < n; cx++)
+ {
+ int westRow = (cx & 1) * cornerRow;
+ int eastRow = ((cx - 1) & 1) * cornerRow;
+ for (int j = 0; j <= n; j++)
+ WalkVisibilityMath.FillClipHeights(
+ (cx + 1) * CellLength + x0, j * CellLength + y0,
+ cyPlane, edgePlanes, grid[eastRow + j]);
+ for (int cy = 0; cy < n; cy++)
+ {
+ if (block.CellInView[n * cx + cy] != WalkBoundingType.Outside)
+ continue; // union across views: never downgrade
+ block.CellInView[n * cx + cy] = WalkVisibilityMath.BlockCheck(
+ grid[westRow + cy], grid[westRow + cy + 1],
+ grid[eastRow + cy], grid[eastRow + cy + 1],
+ edgePlanes.Length, block.MaxZ, block.MinZ);
+ }
+ }
+ }
+}
diff --git a/tests/AcDream.App.Tests/Rendering/Walk/RetailFrameWalkTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/RetailFrameWalkTests.cs
new file mode 100644
index 00000000..471dfd66
--- /dev/null
+++ b/tests/AcDream.App.Tests/Rendering/Walk/RetailFrameWalkTests.cs
@@ -0,0 +1,224 @@
+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 Events = new();
+ public void Emit(in WalkEvent walkEvent) => Events.Add(walkEvent);
+
+ 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 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 IWalkFrameContext CellContext => this;
+ // Permissive near plane: every column wholly inside.
+ public WalkPlane CyPlane => new(new Vector3(0, 0, 1), 0f);
+ public void SetActiveView(WalkPortalView views, int index) { }
+
+ public int ClipBuildingPolygon(
+ WalkBuilding building, WalkPolygon polygon, int side, Span 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());
+ }
+
+ [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());
+ }
+
+ [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);
+ }
+}