Replaces the single scope-global distance-sorted RetailAlphaQueue with
retail's own two independent FIFO lists (CLIP/ALPHA, capacity 3000 each,
D3DPolyRender::AddMeshToAlphaList's exact append-only/capacity-drop
behavior — Ghidra-verified 2026-09-04), routed by a new
RetailAlphaMeshRouter porting DrawMesh's five-row immediate/delayed
branch table and ConstructMesh's subset-mask formula as pure functions,
and drained at retail's four normal-world FlushAlphaList sites
(DrawBuilding/DrawBlock/PView::DrawCells/RenderNormalMode) under the
exact Ghidra-verified no-op predicate (both counts strictly below
threshold*3000). A new WalkFrameEventKind.SortCellExit /
IWalkEventSink.OnSortCellExit / IWalkFrameLeafRenderer.FlushSortCellExit
fires once per admitted land-block cell for DrawBlock's 0.75f valve,
pinned by a dedicated far/near ordering test in RetailFrameWalkTests.cs.
WbDrawDispatcher's two submit sites and ParticleRenderer's one route
through the router; since none of the three ever draws during the Sky
leaf, installs a detail surface, or sets MultiPassAlpha, rows 1/2/4/5
are provably unreachable there and the call sites assert loudly rather
than building unexercisable immediate-draw plumbing. FlushFartherThan,
RetailAlphaOrdering.ComputeViewerDistance, and every viewerDistance
argument on the submit path are deleted.
Scope note (packet s4-depth-alpha-packet.md §10): C4 (routing EnvCell's
transparent shell batches through the shared queue) was not attempted —
EnvCell draws one per-cell MultiDrawIndexedIndirect call with no
per-subset deferred-replay abstraction, and building one without visual
verification (no graphical client in this worktree) was judged out of
this bounded chunk's scope. AP-34 is therefore retired and replaced by
two narrower rows rather than deleted outright: AP-236 (the carried-
forward EnvCell-immediate residual) and AP-237 (a newly identified gap:
TranslucencyKind.AlphaBlend can arise from either retail's Alpha/
Translucent bits, mask 0x02/ALPHA, or the Translucent+ClipMap "cloud"
override, mask 0x08/CLIP — GroupKey doesn't retain the raw bit to tell
them apart, so the router always picks ALPHA; only known example is
cloud GfxObj 0x01004C35). Both are compositing-order-only divergences,
never blend/visual ones.
Mutation checks (each applied, confirmed failing, then reverted):
- FIFO drain order reversed -> 5 RetailAlphaQueueTests fail (order).
- FlushAlphaList `<` -> `<=` -> boundary/scratch tests fail (2250 case
reads drained=0 instead of 2250).
- Capacity check loosened (3000 -> 6000) -> overflow-drop test fails
(TryAppend returns true, PendingCount reads 3001).
- IsFirstForList forced true -> flag test fails once inspected on the
pre-flush two-entry snapshot (the post-flush single-survivor version
of this test was vacuous and rewritten).
- Router row 3 condition inverted -> both the hand-traced Theory (6
cases) and the 160-cell independent-truth-table brute force fail (20
mismatches).
- SortCellExit emitted before OnLandscapeCellTurn instead of after ->
RetailFrameWalkTests ordering pin fails ("SCX must immediately follow
its own cell's SC").
- Prepare-per-list instead of prepare-once-combined -> the CLIP/ALPHA
boundary batching test throws (index out of range).
Gates: Release build 0 warnings/0 errors. Hermetic lane (Lane!=Installed
Dat&...&Status!=KnownFailure) 6855/6855 passed. InstalledDat lane 249
passed / 10 failed — exactly the 4 pre-existing failures (#383 x2
LayoutImporter, TowerAscent KnownFailure, #458 Oh_doorway_still
KnownFailure) plus 6 NEW KnownFailure Facts
(AlphaFlushTranscript_*_MatchesRetailFrame2, one per capture) extending
this gate from PM/PC to AM/FL: the flush SITE+THRESHOLD sequence matches
the capture exactly for all six poses (including zero SortCellExit
drains in every capture, confirming the 0.75 valve is inert at these
scene complexities in both retail and this replay); the drained-COUNT-
per-list dimension diverges because this hermetic harness (matching the
existing PM/PC gate's own EmptyAlphaDepthWorldData design) carries no
live GfxObj/particle content, so every observed count reads (0,0)
against retail's real per-frame volume — both sequences quoted in full
per pose in the packet's new §10. Shader classes (VulkanShaderDescriptor
ContractTests/VulkanShaderManifestTests/RenderPackSpirvValidatorTests)
32/32 passed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
513 lines
22 KiB
C#
513 lines
22 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}");
|
|
|
|
/// <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 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");
|
|
}
|
|
|
|
/// <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);
|
|
}
|
|
}
|