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.
664 lines
28 KiB
C#
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);
|
|
}
|
|
}
|