Ports RenderDeviceD3D::DrawBlock @0x005a17c0's real per-cell order: loop 1 (@0x005a1876) prepares shadow lists; loop 2 (@0x005a197d) DrawLandCell(cell) @0x005a19c0 fires ONLY when the cell is in view, STRICTLY BEFORE DrawSortCell(cell) @0x005a19e6, which fires whenever alwaysDrawObjects (retail default 1 @0x00820ed4) or the cell is in view. RetailFrameWalk.DrawLandscape now emits sink.OnLandCellTurn( landblockId, side, cellIndex) at that exact point, per admitted cell, before the existing DrawBuilding + OnLandscapeCellTurn (the DrawSortCell half). WalkFrameDriver records one LandCell frame event per turn and deletes the whole-stage TerrainSlice(0) emission and the DrawTerrainSlice leaf outright — drawing all terrain before every building let a nearer building's far-Z punch survive under farther terrain drawn afterward, the doorway-behind-a-hill fragment bug from the owner's G2 Holtburg screenshot; this chunk removes it by ORDER alone, with no depth-compare change (S4's punch z-func question is untouched, per the contract). Index-run arithmetic (S3 §9.1 R3): the terrain mesh is cell-major (LandblockMesh.Build: cy outer, cx inner, 6 indices/cell, 384/land- block). A retail LOD cell (side n, LOD coords X,Y) covers cx in [X*8/n,(X+1)*8/n), cy in [Y*8/n,(Y+1)*8/n) — one contiguous run per covered cy row: side 8 -> one run of 6, side 4 -> two runs of 12, side 2 -> four runs of 24; side 1's single coarse cell covers every row contiguously so its 8 per-row runs collapse into ONE run of all 384 indices. TerrainModernRenderer.AppendCellIndexRuns (pure, no GPU, no baked table) and DrawLandCellRuns (resolves the landblock's slot, builds one DrawElementsIndirectCommand per run, reuses the existing DrawRhi bind-and-submit path) implement this; TerrainModernRenderer .Draw(...) is untouched and keeps serving non-walk callers (directional- shadow receivers, the flat terrain path). Order-preserving batching (S3 §9.2 B2): WalkFrameDriver.Replay merges consecutive same-landblock LandCell events with no intervening event into ONE DrawLandCellBatch leaf call; any other event splits the batch. In production this rarely fires because retail's own DrawSortCell (AlwaysDrawObjects=true) always interposes an object- list turn between one cell's LandCell event and the next's — see perfNote in the task report for the resulting command-count increase. Deleted as dead: the _walkTerrainInViewLandcells field and SetWalkTerrainInViewLandcells setter on RetailPViewPassExecutor (fed only DrawWalkTerrainSlice's inViewLandcells filter, which no longer exists — the walk's own per-cell CellInView admission is now the sole terrain-visibility authority) and its two call sites in RetailPViewRenderer.DrawWalkDrivenStatics. Weather placement (S3 §9.1 R5): confirmed unchanged. GameSky's weather pass (RenderWeather, gated on is_player_outside) already runs after every LandCell event for both root kinds — for an outdoor root, DrawLandscapeDynamicsPhase is called directly after driver.Replay() completes (which processes the whole per-cell _events list first); for an interior root with ov>0, it fires via the LandscapeFlush leaf (RetailPViewRenderer.FlushWalkLandscape -> _walkPreClearDynamics), and OnInteriorFloodDrawTurn only emits LandscapeFlush AFTER DrawLandscape's per-cell loop has fully run and recorded every LandCell event ahead of it in the same _events list Replay walks in order. No code change needed; verified by reading the call sites. Tests: WalkEvents/RetailFrameWalk/WalkFrameDriver's existing pins updated (every "TERRAIN:0" expectation deleted, matching the deleted event); new coverage for T1 (WalkLandCellOrderTests — LandWalkOrder + WalkLandscape.CalcDrawOrder + WalkLandscapeAssembler .SideCellCountForRing reproduce both cathedral captures' frame-2 LC/SC sequences byte-for-byte, 533/698 arrival and 531/757 leak, cross- verified against the retail ring-to-LOD table), T2 (a synthetic two- block landscape with a real WalkVisibilityMath-driven out-of-view column, proving LC-before-building/statics, far-to-near, no-LC-but- keeps-SC for the excluded cells, and no TerrainSlice event of any kind), T3 (TerrainLandCellIndexRunsTests — the index-run arithmetic for every side/cell, disjoint and exhaustive over the 384-index landblock), T4 (batching merge/split), and the RetailPViewPassExecutor CompiledCallGraph pin retargeted at DrawWalkLandCellBatch / DrawLandCellRuns. WalkOracleTrace gains LC/SC event kinds and a Load(root, name) overload for the OH capture directory — the one parser change this chunk needs, no validator, no other infrastructure. App hermetic lane: 6,786/6,786 (6,765 baseline + 21 new). InstalledDat lane: 241 passed, 3 accepted failures (2 pre-existing #383 layout fixture-drift tests, 1 TowerAscent Status=KnownFailure) — unchanged from baseline. Core terrain tests: 116/116 (Core untouched by this chunk). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
413 lines
18 KiB
C#
413 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]);
|
|
Assert.DoesNotContain(
|
|
recorder.Combined, e => e.StartsWith("TERRAIN", StringComparison.Ordinal));
|
|
|
|
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);
|
|
}
|
|
}
|