Campaign OVERHAUL S3 chunk 4 fix round 1 (docs/research/2026-09-01-overhaul/s3-walk-ownership-map.md §10.4), on top of 6ba4b0b87. K1, K2, K3, K4, K6, K7, K8 (K5 was the lead's own plan-doc note, already done). K1 (blocking) — the Assert.Single pin over DrawLandscapeDynamicsPhase's DrawWeatherOnce call site stayed green even wrapped in a foreach (it counts distinct call-site offsets, not loop shape). Added a real LOOP-SHAPE pin reusing CompiledCallGraph.ReadBranches: asserts no backward branch (target offset < its own offset) spans the DrawWeatherOnce call's IL offset — the shape every C# for/foreach/while loop compiles to. K2 (minor) — the Collect-time "OC" print fired on ctx.WeatherGateOpen alone while the Replay-time draw independently re-derived clipAssembly.OutsideViewSlices.Length > 0, which could diverge on an interior root whose landscape turn ran but whose reassembled outside-view slices ended up empty. WalkFrameDriver now owns one WeatherTurnFired flag, set unconditionally inside OnWeatherTurn (so it tracks "did the print's own gate hold" regardless of the transcript flag) and reset every BeginFrame/AbortFrame; RetailPViewRenderer.DrawLandscapeDynamicsPhase gates DrawWeatherOnce on it instead of re-deriving its own condition. K3 (minor) — ClipFrame.SetTerrainClip (the terrain OutsideView writer) had no remaining caller after chunk 4's original round and K4 below; deleted along with IWorldPassSurface.SetTerrainClip, RhiWorldPassSurface.SetTerrainClip, the now-orphaned PublishTerrainClip helper, and PrepareClipFrame's terrain-clip publish call. The terrain/sky shaders still declare the TerrainClip UBO (verified: terrain_modern.vert, terrain_atmospheric.vert, sky.vert all read uTerrainClipCount/uTerrainClipPlanes), so the section binding itself (WorldFrameSectionBinding.BindTerrainClip) stays — its existing zeroed-ring fallback (for when nothing published the section) now binds that same all-zero disabled block on every frame, identical bytes to the old permanent NoClip/Reset default. K4 (major) — DrawWalkSky still looped the OUTDOOR case once per active landscape view under a doorway scissor + BindTerrainClip + EnableClipDistances (the INTERIOR case already drew once unclipped, FW4 slice 6). Retail draws GameSky::Draw(sky,0) ONCE, unconditionally, before LScape::draw's block loop, for both root kinds. Collapsed DrawWalkSky to one unconditional, unclipped draw; deleted BeginDoorwayScissor and the RetailPViewPassExecutor.EnableClipDistances wrapper (both lost their only caller). _surface.BeginScissor/EndScissor and IWorldPassSurface.EnableClipDistances stay: RhiWorldPassSurface.ClearInteriorDepth still ends an active scissor, and WorldScenePassExecutor (the separate flat-world path) still calls EnableClipDistances directly. K6 (minor) — the punch-fan CPU/GPU equivalence pin hand-built a ClipViewSlice from ClipPlaneSet.From's raw output, which could pass even if ClipFrameAssembler.Assemble's own packing/array-construction diverged from that output. Rewrote it to build a real PortalVisibilityFrame and run it through ClipFrameAssembler.Assemble, reading the planes back through assembly.OutsideViewSlices[0].Planes — the exact outsideSlicesList.Add(new ClipViewSlice(slot, AabbOf(poly), planes)) path ReassembleOutsideViewFromWalk (the walk's real interior-root producer) shares. K7 (note) — restored the "a real look-in slice never reuses the reserved no-clip slot 0" assertion the deleted VisibleClipSlotsInLookInTurn used to prove, via a new internal test-only accessor (WalkFrameDriver.LookInSliceClipSlotAt) reading the same _lookInSlices storage InteriorFloodViewClipPlanesAt resolves through _clipFrame.GetSlotPlanes(slice.ClipSlot). K8 (note) — documented in DrawWeatherOnce's own comment that the weather mesh drawing before the rain particles is this method's own call-order choice; retail's single GameSky::Draw(sky,1) imposes no order between acdream's two substitutes. Every new/changed pin's mutation was hand-verified this session (temporarily applied, ran the specific test, confirmed the exact failing assertion, then reverted — see the parent task's structured report for the four failing-assertion texts). No register row added or removed — every change here deletes an acdream-only rule or repairs a pin; none introduces a new deviation. Full solution build: 0 warnings/0 errors. App hermetic (Lane!=InstalledDat/PreparedPackage/Live/ Manual/Timing/Windows/Linux/SystemFont & Purpose!=Diagnostic & Status!=KnownFailure): 6831/6831. InstalledDat: 244 pass/1 skip/4 known — identical to 6ba4b0b87's own baseline (2x #383 layout tests, TowerAscent, and the pre-existing #458 WalkLandscape.CheckBlocks block-visibility divergence, unrelated to and untouched by this round). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
789 lines
38 KiB
C#
789 lines
38 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Gpu;
|
|
using AcDream.App.Rendering.Walk;
|
|
using AcDream.App.Tests.Rendering;
|
|
using AcDream.Core.Rendering;
|
|
|
|
namespace AcDream.App.Tests.Rendering.Walk;
|
|
|
|
/// <summary>
|
|
/// Campaign OVERHAUL S3 chunk 1 (§11.2): tests T1/T2 for the print-only walk
|
|
/// transcript emitter (<see cref="WalkTranscriptDump"/>, gated by
|
|
/// <see cref="RenderingDiagnostics.DumpWalkTranscriptEnabled"/>), plus the
|
|
/// §11.2 B4 offline signature-diff self-check. Reuses
|
|
/// <see cref="WalkFrameDriverTests"/>'s own private fixture types via the
|
|
/// shared <c>partial class</c> — the SAME minimal interior two-cell flood
|
|
/// (one exit view) that file's own
|
|
/// <c>RunFrame_InteriorFloodWithExitView_FreshDriverSkipsTheGatedClearThenDrawsSealsAndFloodCells</c>
|
|
/// test already exercises and proves correct; here only
|
|
/// <see cref="WalkFrameDriver.Collect"/> runs (no <c>Replay</c>/GPU
|
|
/// submission), since every transcript print fires synchronously during the
|
|
/// walk itself.
|
|
///
|
|
/// <para>
|
|
/// <see cref="RenderingDiagnostics.DumpWalkTranscriptEnabled"/> and
|
|
/// <see cref="Console.Out"/> are both process-wide mutable statics — joins
|
|
/// <see cref="CameraDiagnosticsCollection"/> for the SAME reason
|
|
/// <c>CornerFloodReplayTests</c>/<c>Issue181WallPressEquilibriumTests</c> do
|
|
/// (that collection's own doc comment, issue #251): interleaved
|
|
/// <c>Console.SetOut</c> redirection across parallel test classes can
|
|
/// restore a disposed <see cref="StringWriter"/> process-wide.
|
|
/// </para>
|
|
/// </summary>
|
|
[Collection(CameraDiagnosticsCollection.Name)]
|
|
public sealed partial class WalkFrameDriverTests
|
|
{
|
|
private static (WalkCell Cell1, WalkCell Cell2) BuildTranscriptFixtureCells(TestContext ctx)
|
|
{
|
|
var cell1 = new WalkCell
|
|
{
|
|
CellId = 0x100,
|
|
StabList = [0x101u],
|
|
Portals =
|
|
[
|
|
new WalkCellPortal
|
|
{
|
|
OtherCellId = 0x101, PolygonIndex = 0, PortalSide = 0, OtherPortalId = 0,
|
|
},
|
|
// The exit portal — raises ov to 1, so the flood draws the
|
|
// (empty, zero-block) landscape too, exercising LS.
|
|
new WalkCellPortal
|
|
{
|
|
OtherCellId = 0xFFFFFFFF, PolygonIndex = 1, PortalSide = 0, OtherPortalId = -1,
|
|
},
|
|
],
|
|
PortalPolygons = [Quad(-2f), Quad(-3f)],
|
|
};
|
|
var cell2 = new WalkCell
|
|
{
|
|
CellId = 0x101,
|
|
Portals = [new WalkCellPortal
|
|
{
|
|
OtherCellId = 0x100, PolygonIndex = 0, PortalSide = 1, OtherPortalId = 0,
|
|
}],
|
|
PortalPolygons = [Quad(-2f)],
|
|
};
|
|
ctx.Cells[cell1.CellId] = cell1;
|
|
ctx.Cells[cell2.CellId] = cell2;
|
|
return (cell1, cell2);
|
|
}
|
|
|
|
/// <summary>T1 (§11.3): the flag off — zero output, and the emitter's
|
|
/// own methods (the only code this chunk adds to the walk path)
|
|
/// allocate nothing. The walk itself is NOT independently zero-alloc
|
|
/// (e.g. <c>RetailFrameWalk.EmitDrawCells</c> allocates its cell-id
|
|
/// array on every call — pre-existing, unrelated to this chunk), so the
|
|
/// allocation bound below is scoped to <see cref="WalkTranscriptDump"/>
|
|
/// itself, matching B1's actual contract ("allocates nothing EXTRA").</summary>
|
|
[Fact]
|
|
public void TranscriptEmitter_FlagOff_EveryPrintMethodIsAZeroCostNoOp()
|
|
{
|
|
bool previous = RenderingDiagnostics.DumpWalkTranscriptEnabled;
|
|
RenderingDiagnostics.DumpWalkTranscriptEnabled = false;
|
|
TextWriter originalOut = Console.Out;
|
|
var capture = new StringWriter();
|
|
try
|
|
{
|
|
Console.SetOut(capture);
|
|
|
|
var cells = new List<uint> { 0x100u, 0x101u };
|
|
void Step()
|
|
{
|
|
WalkTranscriptDump.PrintFrameRoot(1, 0x100u, Vector3.Zero, Vector3.UnitY);
|
|
WalkTranscriptDump.PrintLandscape();
|
|
WalkTranscriptDump.PrintBuilding(0x100u);
|
|
WalkTranscriptDump.PrintDrawInside(0x100u);
|
|
WalkTranscriptDump.PrintDrawCells(outdoorPview: false, 1, cells);
|
|
WalkTranscriptDump.PrintLandCell(0xF4180001u);
|
|
WalkTranscriptDump.PrintSortCell(0xF4180001u);
|
|
WalkTranscriptDump.PrintEnvCellShell(0x101u);
|
|
WalkTranscriptDump.PrintObjectCellTurn(0x101u);
|
|
}
|
|
|
|
ZeroAllocationProbe.AssertAllocatesNothing(
|
|
"WalkTranscriptDump.Print* (flag off)", Step);
|
|
|
|
AssertNoTranscriptLines(capture);
|
|
}
|
|
finally
|
|
{
|
|
Console.SetOut(originalOut);
|
|
RenderingDiagnostics.DumpWalkTranscriptEnabled = previous;
|
|
}
|
|
}
|
|
|
|
/// <summary>T1's integration half: a full synthetic interior frame,
|
|
/// driven through the SAME <see cref="WalkFrameDriver.Collect"/> path
|
|
/// production uses, produces literally NO transcript output when the
|
|
/// flag is off. S3 chunk 1 fix round 1 (G1): TWO <c>Collect</c> calls,
|
|
/// not one — <see cref="WalkOracleTrace.Parse"/> structurally discards
|
|
/// the last (open) frame (its own class doc: the capture harness
|
|
/// detaches at the frame marker, so the final "F n" never records its
|
|
/// own draws), so a SINGLE-frame transcript parses to zero frames
|
|
/// whether or not the flag actually gated anything — a one-Collect-call
|
|
/// version of this test is vacuous (it would pass exactly as well with
|
|
/// the flag gate forced true). Two Collect calls give the parser a real
|
|
/// COMPLETE frame to see if the gate leaked (MUTATION CHECK: forcing
|
|
/// <c>RenderingDiagnostics.DumpWalkTranscriptEnabled</c> true right
|
|
/// after setting it false here makes <c>AssertNoTranscriptLines</c>
|
|
/// fail, both on the parsed-frame-count assertion and the prefix
|
|
/// scan).</summary>
|
|
[Fact]
|
|
public void Collect_TranscriptFlagOff_ProducesNoConsoleOutput()
|
|
{
|
|
bool previous = RenderingDiagnostics.DumpWalkTranscriptEnabled;
|
|
RenderingDiagnostics.DumpWalkTranscriptEnabled = false;
|
|
TextWriter originalOut = Console.Out;
|
|
var capture = new StringWriter();
|
|
try
|
|
{
|
|
Console.SetOut(capture);
|
|
|
|
using var fx = new DispatcherFixture();
|
|
var ctx = new TestContext();
|
|
(WalkCell cell1, _) = BuildTranscriptFixtureCells(ctx);
|
|
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
|
|
var worldData = new FakeWorldData();
|
|
var leaf = new RecordingLeafRenderer(new List<string>());
|
|
using ClipFrame clipFrame = ClipFrame.NoClip();
|
|
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData, clipFrame: clipFrame);
|
|
var walk = new RetailFrameWalk();
|
|
|
|
driver.Collect(
|
|
walk, cell1.CellId, cell1, landscape, ctx, Matrix4x4.Identity, Vector3.Zero);
|
|
driver.Collect(
|
|
walk, cell1.CellId, cell1, landscape, ctx, Matrix4x4.Identity, Vector3.Zero);
|
|
|
|
AssertNoTranscriptLines(capture);
|
|
}
|
|
finally
|
|
{
|
|
Console.SetOut(originalOut);
|
|
RenderingDiagnostics.DumpWalkTranscriptEnabled = previous;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The ten transcript line-kind prefixes <see cref="WalkTranscriptDump"/>
|
|
/// ever emits — <c>F </c>/<c>P </c>/<c>LS</c>/<c>LC </c>/<c>SC </c>/
|
|
/// <c>BLD </c>/<c>DI </c>/<c>DC </c>/<c>EC </c>/<c>OC </c> — matching
|
|
/// each print method's own exact <c>Console.WriteLine</c> format
|
|
/// (S3 chunk 1 fix round 1, G1).
|
|
/// </summary>
|
|
private static readonly string[] TranscriptLinePrefixes =
|
|
["F ", "P ", "LS", "LC ", "SC ", "BLD ", "DI ", "DC ", "EC ", "OC "];
|
|
|
|
/// <summary>
|
|
/// Asserts NONE of <see cref="WalkTranscriptDump"/>'s own line kinds
|
|
/// appear in <paramref name="capture"/> — robust to unrelated
|
|
/// <see cref="Console"/> noise from another test class running in
|
|
/// parallel (a real, observed hazard: xUnit runs distinct classes
|
|
/// concurrently by default, and <see cref="Console.Out"/> is a
|
|
/// process-wide static — see <c>CameraDiagnosticsCollection</c>'s own
|
|
/// doc comment, issue #251). A plain <c>Assert.Empty(capture.ToString())</c>
|
|
/// is NOT this robust: an unrelated class's unconditional
|
|
/// <c>Console.WriteLine</c> can land inside this test's redirect window
|
|
/// and fail it for a reason that has nothing to do with the transcript
|
|
/// flag. <see cref="WalkOracleTrace.Parse"/> only ever appends a frame
|
|
/// once it sees a well-formed <c>F <n></c> marker line — arbitrary
|
|
/// unrelated text can never satisfy that regex, so a truly EMPTY parsed
|
|
/// frame list is exactly as strong a proof that THIS code printed
|
|
/// nothing, without being sensitive to what else shares the process
|
|
/// console. S3 chunk 1 fix round 1 (G1) adds a second, independent
|
|
/// check: no captured line starts with any of the ten transcript
|
|
/// prefixes — those exact tokens never begin an unrelated test class's
|
|
/// own console noise, so this stays robust to the SAME parallel hazard
|
|
/// while also catching a leak the parser's own drop-last-frame quirk
|
|
/// could otherwise hide (a caller that (mistakenly) drives only ONE
|
|
/// frame).
|
|
/// </summary>
|
|
private static void AssertNoTranscriptLines(StringWriter capture)
|
|
{
|
|
string[] lines = capture.ToString()
|
|
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(l => l.TrimEnd('\r'))
|
|
.ToArray();
|
|
Assert.Empty(WalkOracleTrace.Parse(lines));
|
|
Assert.DoesNotContain(
|
|
lines,
|
|
l => Array.Exists(
|
|
TranscriptLinePrefixes, p => l.StartsWith(p, StringComparison.Ordinal)));
|
|
}
|
|
|
|
/// <summary>T2 (§11.3): flag on, one synthetic interior frame — the
|
|
/// printed lines parse with the extended <see cref="WalkOracleTrace"/>
|
|
/// into the same events the driver recorded (emitter/parser round
|
|
/// trip). Asserted structurally (kinds, cell ids, counts) rather than
|
|
/// against a hand-predicted exact ordering, so the test does not
|
|
/// silently pin an assumption about traversal order it never
|
|
/// independently verified.</summary>
|
|
[Fact]
|
|
public void Collect_TranscriptFlagOn_PrintedLinesRoundTripThroughTheParser()
|
|
{
|
|
bool previous = RenderingDiagnostics.DumpWalkTranscriptEnabled;
|
|
RenderingDiagnostics.DumpWalkTranscriptEnabled = true;
|
|
TextWriter originalOut = Console.Out;
|
|
var capture = new StringWriter();
|
|
try
|
|
{
|
|
Console.SetOut(capture);
|
|
|
|
using var fx = new DispatcherFixture();
|
|
var ctx = new TestContext();
|
|
(WalkCell cell1, WalkCell cell2) = BuildTranscriptFixtureCells(ctx);
|
|
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
|
|
var worldData = new FakeWorldData();
|
|
var leaf = new RecordingLeafRenderer(new List<string>());
|
|
using ClipFrame clipFrame = ClipFrame.NoClip();
|
|
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData, clipFrame: clipFrame);
|
|
var walk = new RetailFrameWalk();
|
|
|
|
// Two Collect calls: WalkOracleTrace.Parse (like every real
|
|
// capture) only flushes a frame once the NEXT "F n" marker
|
|
// appears — it deliberately drops the final in-progress frame
|
|
// (the detach-frame rule). One frame alone would parse to zero
|
|
// complete frames.
|
|
driver.Collect(
|
|
walk, cell1.CellId, cell1, landscape, ctx, Matrix4x4.Identity, Vector3.Zero);
|
|
driver.Collect(
|
|
walk, cell1.CellId, cell1, landscape, ctx, Matrix4x4.Identity, Vector3.Zero);
|
|
|
|
Console.Out.Flush();
|
|
string[] lines = capture.ToString()
|
|
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(l => l.TrimEnd('\r'))
|
|
.ToArray();
|
|
Assert.NotEmpty(lines);
|
|
|
|
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Parse(lines);
|
|
WalkOracleFrame frame = Assert.Single(frames);
|
|
Assert.Equal(1, frame.Number);
|
|
|
|
Assert.NotNull(frame.Pose);
|
|
Assert.Equal(cell1.CellId, frame.Pose!.CellId);
|
|
|
|
Assert.Equal(cell1.CellId, frame.InteriorRootCell);
|
|
Assert.True(frame.HasLandscape);
|
|
|
|
WalkOracleEvent dc = Assert.Single(
|
|
frame.Events, e => e.Kind == WalkOracleEventKind.DrawCells);
|
|
Assert.Equal(1, dc.OutsideViewCount);
|
|
Assert.Equal(
|
|
new HashSet<uint> { cell1.CellId, cell2.CellId },
|
|
dc.Cells.ToHashSet());
|
|
|
|
// The interior root's own flood visits both cells for a shell
|
|
// AND an object-list turn — EC/OC pair, one line per cell, no
|
|
// dedupe (this is the interior root's OWN flood — the ONE case
|
|
// WalkTranscriptDump.PrintEnvCellShell's own doc comment (G9)
|
|
// still guarantees EC==OC count for; a look-in flood matches
|
|
// too, but G8's per-frame trailing weather OC does not, and
|
|
// never fires here since this fixture's landscape has zero
|
|
// blocks/buildings).
|
|
List<uint> ec = frame.Events
|
|
.Where(e => e.Kind == WalkOracleEventKind.EnvCellShell)
|
|
.Select(e => e.CellId!.Value).ToList();
|
|
List<uint> oc = frame.Events
|
|
.Where(e => e.Kind == WalkOracleEventKind.ObjectCellTurn)
|
|
.Select(e => e.CellId!.Value).ToList();
|
|
Assert.Equal(new HashSet<uint> { cell1.CellId, cell2.CellId }, ec.ToHashSet());
|
|
Assert.Equal(new HashSet<uint> { cell1.CellId, cell2.CellId }, oc.ToHashSet());
|
|
Assert.Equal(2, ec.Count);
|
|
Assert.Equal(2, oc.Count);
|
|
|
|
// No landblocks were published (the stub 1x1 landscape), so no
|
|
// LC/SC/BLD turns exist this frame — the transcript format and
|
|
// relative order for those three kinds is proven by
|
|
// OutdoorRoot_TranscriptFlagOn_PrintsLandscapeThenLandCellThenSortCellThenBuilding_InOrder
|
|
// below (S3 chunk 1 fix round 1, G3), and separately against
|
|
// real retail data by WalkOracleTraceTests.AllFixtures and
|
|
// WalkLandCellOrderTests.
|
|
Assert.DoesNotContain(frame.Events, e => e.Kind
|
|
is WalkOracleEventKind.LandCell or WalkOracleEventKind.SortCell
|
|
or WalkOracleEventKind.Building);
|
|
}
|
|
finally
|
|
{
|
|
Console.SetOut(originalOut);
|
|
RenderingDiagnostics.DumpWalkTranscriptEnabled = previous;
|
|
}
|
|
}
|
|
|
|
/// <summary>S3 chunk 1 fix round 1 (G3): the outdoor-root half T2 never
|
|
/// covered — the flag-on round trip above uses a stub 1x1 landscape with
|
|
/// zero blocks, so LC/SC/BLD never fire at all. Drives
|
|
/// <see cref="RetailFrameWalk.DrawLandscape"/> directly with the SAME
|
|
/// deterministic "CY-only" <see cref="OneDegenerateView"/> admission
|
|
/// technique <c>OutdoorRoot_LandCellPrecedesItsOwnCellsObjectTurn_ThenFlushesAtReplaysEnd</c>
|
|
/// uses (a real <see cref="RetailFrameWalk.WalkFrame"/> root's default
|
|
/// full-viewport view depends on this suite's synthetic ray caster,
|
|
/// which does not model screen geometry faithfully enough to predict
|
|
/// block/cell admission from — see that test's own doc comment), with a
|
|
/// building attached to the one admitted cell so BLD fires too. Checked
|
|
/// against the RAW captured lines directly (not
|
|
/// <see cref="WalkOracleTrace.Parse"/>) — this test only needs kind
|
|
/// presence and relative order, not a complete F/P-framed capture.
|
|
/// MUTATION CHECK: deleting <c>WalkTranscriptDump.PrintLandCell</c>'s
|
|
/// call site (or its body) makes the "expected an LC line" assertion
|
|
/// fail.</summary>
|
|
[Fact]
|
|
public void OutdoorRoot_TranscriptFlagOn_PrintsLandscapeThenLandCellThenSortCellThenBuilding_InOrder()
|
|
{
|
|
bool previous = RenderingDiagnostics.DumpWalkTranscriptEnabled;
|
|
RenderingDiagnostics.DumpWalkTranscriptEnabled = true;
|
|
TextWriter originalOut = Console.Out;
|
|
var capture = new StringWriter();
|
|
try
|
|
{
|
|
Console.SetOut(capture);
|
|
|
|
using var fx = new DispatcherFixture();
|
|
var leaf = new RecordingLeafRenderer(new List<string>());
|
|
leaf.CellsWithoutEmitters.UnionWith(CoarseLandscapeBuckets(0xF4180000u));
|
|
var ctx = new TestContext();
|
|
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, new FakeWorldData());
|
|
var walk = new RetailFrameWalk();
|
|
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
|
|
var block = new WalkLandBlock
|
|
{
|
|
LandblockId = 0xF4180000u, SideCellCount = 1, MaxZ = 10f, MinZ = 0f,
|
|
};
|
|
block.EnsureCellArrays();
|
|
block.CellBuildings[0] = new WalkBuilding { PositionCellId = 0xF4180000u };
|
|
landscape.Blocks[0] = block;
|
|
|
|
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
|
|
walk.DrawLandscape(landscape, OneDegenerateView(), ctx, driver);
|
|
driver.EndFrame();
|
|
|
|
Console.Out.Flush();
|
|
string[] lines = capture.ToString()
|
|
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(l => l.TrimEnd('\r'))
|
|
.ToArray();
|
|
|
|
int lsIndex = Array.IndexOf(lines, "LS");
|
|
int lcIndex = Array.FindIndex(
|
|
lines, l => l.StartsWith("LC ", StringComparison.Ordinal));
|
|
int scIndex = Array.FindIndex(
|
|
lines, l => l.StartsWith("SC ", StringComparison.Ordinal));
|
|
int bldIndex = Array.FindIndex(
|
|
lines, l => l.StartsWith("BLD ", StringComparison.Ordinal));
|
|
|
|
Assert.True(lsIndex >= 0, "expected an LS line");
|
|
Assert.True(lcIndex >= 0, "expected an LC line");
|
|
Assert.True(scIndex >= 0, "expected an SC line");
|
|
Assert.True(bldIndex >= 0, "expected a BLD line");
|
|
Assert.True(lsIndex < lcIndex, "LS must precede LC");
|
|
Assert.True(
|
|
lcIndex < scIndex,
|
|
"LC must precede SC — RenderDeviceD3D::DrawLandCell before DrawSortCell");
|
|
Assert.True(
|
|
scIndex < bldIndex,
|
|
"SC must precede BLD — S3 chunk 1 fix round 1 (G7): DrawSortCell entry, "
|
|
+ "before its own DrawBuilding call");
|
|
Assert.Equal("LC f4180001", lines[lcIndex]);
|
|
Assert.Equal("SC f4180001", lines[scIndex]);
|
|
Assert.Equal("BLD f4180000", lines[bldIndex]);
|
|
}
|
|
finally
|
|
{
|
|
Console.SetOut(originalOut);
|
|
RenderingDiagnostics.DumpWalkTranscriptEnabled = previous;
|
|
}
|
|
}
|
|
|
|
/// <summary>§11.2 B4: the offline signature diff, self-checked over a
|
|
/// synthetic pair — the SAME captured T2 transcript versus itself with
|
|
/// its own LAST event removed. Reuses <c>Collect_TranscriptFlagOn_…</c>'s
|
|
/// own capture rather than duplicating the walk drive.</summary>
|
|
[Fact]
|
|
public void SignatureDiff_ReportsTheExactRemovedEvent()
|
|
{
|
|
bool previous = RenderingDiagnostics.DumpWalkTranscriptEnabled;
|
|
RenderingDiagnostics.DumpWalkTranscriptEnabled = true;
|
|
TextWriter originalOut = Console.Out;
|
|
var capture = new StringWriter();
|
|
try
|
|
{
|
|
Console.SetOut(capture);
|
|
|
|
using var fx = new DispatcherFixture();
|
|
var ctx = new TestContext();
|
|
(WalkCell cell1, _) = BuildTranscriptFixtureCells(ctx);
|
|
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
|
|
var worldData = new FakeWorldData();
|
|
var leaf = new RecordingLeafRenderer(new List<string>());
|
|
using ClipFrame clipFrame = ClipFrame.NoClip();
|
|
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData, clipFrame: clipFrame);
|
|
var walk = new RetailFrameWalk();
|
|
|
|
// Two Collect calls — see the round-trip test's own comment on
|
|
// why one alone parses to zero complete frames.
|
|
driver.Collect(
|
|
walk, cell1.CellId, cell1, landscape, ctx, Matrix4x4.Identity, Vector3.Zero);
|
|
driver.Collect(
|
|
walk, cell1.CellId, cell1, landscape, ctx, Matrix4x4.Identity, Vector3.Zero);
|
|
|
|
Console.Out.Flush();
|
|
string[] lines = capture.ToString()
|
|
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(l => l.TrimEnd('\r'))
|
|
.ToArray();
|
|
|
|
IReadOnlyList<WalkOracleFrame> expected = WalkOracleTrace.Parse(lines);
|
|
WalkOracleFrame expectedFrame = Assert.Single(expected);
|
|
Assert.NotEmpty(expectedFrame.Events);
|
|
|
|
// Self-vs-self (unmutated copy): no divergence at all.
|
|
IReadOnlyList<WalkOracleFrame> unchanged =
|
|
[expectedFrame with { Events = expectedFrame.Events.ToList() }];
|
|
Assert.Null(WalkTranscriptSignatureDiff.FirstDivergence(expected, unchanged));
|
|
|
|
// Self-vs-self-minus-one (the LAST event removed): the reported
|
|
// divergence is exact — same frame number, position exactly at
|
|
// the new (shorter) end, expected = the removed event's own
|
|
// signature string, actual = "<end of frame>".
|
|
int lastIndex = expectedFrame.Events.Count - 1;
|
|
List<WalkOracleEvent> truncated = expectedFrame.Events.Take(lastIndex).ToList();
|
|
IReadOnlyList<WalkOracleFrame> actual =
|
|
[expectedFrame with { Events = truncated }];
|
|
|
|
WalkTranscriptSignatureDiff.Divergence? divergence =
|
|
WalkTranscriptSignatureDiff.FirstDivergence(expected, actual);
|
|
|
|
Assert.NotNull(divergence);
|
|
Assert.Equal(expectedFrame.Number, divergence!.Value.FrameNumber);
|
|
Assert.Equal(lastIndex, divergence.Value.Position);
|
|
Assert.Equal("<end of frame>", divergence.Value.Actual);
|
|
Assert.NotEqual("<end of frame>", divergence.Value.Expected);
|
|
}
|
|
finally
|
|
{
|
|
Console.SetOut(originalOut);
|
|
RenderingDiagnostics.DumpWalkTranscriptEnabled = previous;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Campaign OVERHAUL S3 chunk 4 (O1/O2): a REAL once-per-frame pin for
|
|
/// the weather "OC" transcript line, observed through the printed
|
|
/// lines produced by driving the actual <see cref="RetailFrameWalk"/> +
|
|
/// <see cref="WalkFrameDriver"/> pair — not IL offsets (the ledger's
|
|
/// own finding: an IL-offset "call order" check cannot fail when the
|
|
/// call is moved back inside a loop the reviewers can reproduce with the
|
|
/// suite green). An outdoor root's <c>DrawLandscape</c> is the LAST
|
|
/// thing <see cref="RetailFrameWalk.WalkFrame"/> does, so retail's
|
|
/// <c>GameSky::Draw(sky,1)</c> turn — and therefore this "OC" line — is
|
|
/// the frame's last event.
|
|
/// <para>
|
|
/// MUTATION (a, print twice): if <c>IWalkEventSink.OnWeatherTurn</c>
|
|
/// fired twice (e.g. a regression reintroducing a per-slice loop), the
|
|
/// <c>Assert.Single</c> below would see 2 matches and fail with
|
|
/// "Collection contained more than one match".
|
|
/// </para>
|
|
/// <para>
|
|
/// MUTATION (b, wrong cell): the print always carries whatever <see
|
|
/// cref="IRetailFrameWalkContext.ViewerCellId"/> the context supplies
|
|
/// (production wires <c>RetailPViewFrameInput.ViewerCellId</c>, never
|
|
/// <c>PlayerCellId</c> — see <c>WalkProductionFrameContext</c>'s ctor/
|
|
/// <c>Reset</c> call sites in <c>RetailPViewRenderer.DrawInside</c>); if
|
|
/// production started wiring <c>PlayerCellId</c> instead, this test's
|
|
/// own <see cref="TestContext.ViewerCellId"/> would still print
|
|
/// correctly (the walk layer only ever sees the ONE value it is
|
|
/// handed) — the <c>Assert.Equal(ctx.ViewerCellId, ...)</c> below pins
|
|
/// that the walk layer's own plumbing never substitutes a different
|
|
/// value of its own.
|
|
/// </para>
|
|
/// <para>
|
|
/// MUTATION (c, wrong time): if the print instead fired lazily at
|
|
/// Replay time (as it used to, from <c>DrawWeatherOnce</c>) this test —
|
|
/// which only calls <see cref="WalkFrameDriver.Collect"/>, never
|
|
/// <see cref="WalkFrameDriver.Replay"/> — would show ZERO
|
|
/// <see cref="WalkOracleEventKind.ObjectCellTurn"/> events, and
|
|
/// <c>Assert.Single</c> would fail with "no match" instead of finding
|
|
/// exactly one.
|
|
/// </para>
|
|
/// </summary>
|
|
[Fact]
|
|
public void Collect_OutdoorRoot_WeatherGateOpen_PrintsExactlyOneOcAtFrameEndMatchingTheViewerCell()
|
|
{
|
|
bool previous = RenderingDiagnostics.DumpWalkTranscriptEnabled;
|
|
RenderingDiagnostics.DumpWalkTranscriptEnabled = true;
|
|
TextWriter originalOut = Console.Out;
|
|
var capture = new StringWriter();
|
|
try
|
|
{
|
|
Console.SetOut(capture);
|
|
|
|
using var fx = new DispatcherFixture();
|
|
const uint viewerCellId = 0xF4180003u; // low word < 0x100 -> outdoor root
|
|
var ctx = new TestContext { WeatherGateOpen = true, ViewerCellId = viewerCellId };
|
|
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
|
|
var leaf = new RecordingLeafRenderer(new List<string>());
|
|
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, new FakeWorldData());
|
|
var walk = new RetailFrameWalk();
|
|
|
|
// Two Collect calls — WalkOracleTrace.Parse (like every real
|
|
// capture) discards the final in-progress frame.
|
|
driver.Collect(
|
|
walk, viewerCellId, null, landscape, ctx, Matrix4x4.Identity, Vector3.Zero);
|
|
driver.Collect(
|
|
walk, viewerCellId, null, landscape, ctx, Matrix4x4.Identity, Vector3.Zero);
|
|
|
|
Console.Out.Flush();
|
|
string[] lines = capture.ToString()
|
|
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(l => l.TrimEnd('\r'))
|
|
.ToArray();
|
|
|
|
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Parse(lines);
|
|
WalkOracleFrame frame = Assert.Single(frames);
|
|
Assert.NotNull(frame.Pose);
|
|
Assert.Equal(viewerCellId, frame.Pose!.CellId);
|
|
|
|
WalkOracleEvent oc = Assert.Single(
|
|
frame.Events, e => e.Kind == WalkOracleEventKind.ObjectCellTurn);
|
|
Assert.Equal(viewerCellId, oc.CellId);
|
|
Assert.Same(frame.Events[frame.Events.Count - 1], oc);
|
|
}
|
|
finally
|
|
{
|
|
Console.SetOut(originalOut);
|
|
RenderingDiagnostics.DumpWalkTranscriptEnabled = previous;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Campaign OVERHAUL S3 chunk 4 (O1/O2/O3): the interior-root sibling of
|
|
/// <see cref="Collect_OutdoorRoot_WeatherGateOpen_PrintsExactlyOneOcAtFrameEndMatchingTheViewerCell"/> —
|
|
/// retail's <c>GameSky::Draw(sky,1)</c> runs at the END of <c>LScape::
|
|
/// draw</c>, which an interior root calls from <c>DrawInside</c> BEFORE
|
|
/// <c>OnInteriorFloodDrawTurn</c> draws the flood's own cells (first the
|
|
/// reverse EnvCell-shell loop, then the reverse object-list loop) — so
|
|
/// the weather "OC" line must sit strictly between the "LS" line and the
|
|
/// flood's FIRST "EC" line, never mixed in among the flood's own EC/OC
|
|
/// pairs (which fire after it, per-cell, from
|
|
/// <c>WalkFrameDriver.EmitFloodTurns</c>).
|
|
/// <para>
|
|
/// MUTATION (a, print twice): a second <c>OnWeatherTurn</c> firing would
|
|
/// add a second <see cref="WalkOracleEventKind.ObjectCellTurn"/> to the
|
|
/// LS-to-first-EC window, failing <c>Assert.Single</c> on the filtered
|
|
/// list below.
|
|
/// </para>
|
|
/// <para>
|
|
/// MUTATION (c, wrong time — the ledger's actual round-1 regression):
|
|
/// the OLD call site printed the weather OC from the Replay-time
|
|
/// <c>LandscapeFlush</c> leaf, i.e. at the FRAME'S END (after every
|
|
/// flood EC/OC), not between the landscape and the flood. Moving the
|
|
/// print there again would move it OUT of the LS-to-first-EC window,
|
|
/// making the windowed <c>Assert.Single</c> below find zero matches
|
|
/// instead of one.
|
|
/// </para>
|
|
/// </summary>
|
|
[Fact]
|
|
public void Collect_InteriorRoot_WeatherGateOpen_PrintsOneOcBetweenLandscapeAndTheFloodsFirstShell()
|
|
{
|
|
bool previous = RenderingDiagnostics.DumpWalkTranscriptEnabled;
|
|
RenderingDiagnostics.DumpWalkTranscriptEnabled = true;
|
|
TextWriter originalOut = Console.Out;
|
|
var capture = new StringWriter();
|
|
try
|
|
{
|
|
Console.SetOut(capture);
|
|
|
|
using var fx = new DispatcherFixture();
|
|
var ctx = new TestContext();
|
|
(WalkCell cell1, WalkCell cell2) = BuildTranscriptFixtureCells(ctx);
|
|
// Production always hands the walk the SAME cell id as both the
|
|
// Collect root and the "P" line's own cell (ctx.ViewerCellId ==
|
|
// cameraCellId at the real call site).
|
|
ctx.ViewerCellId = cell1.CellId;
|
|
ctx.WeatherGateOpen = true;
|
|
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
|
|
var worldData = new FakeWorldData();
|
|
var leaf = new RecordingLeafRenderer(new List<string>());
|
|
using ClipFrame clipFrame = ClipFrame.NoClip();
|
|
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData, clipFrame: clipFrame);
|
|
var walk = new RetailFrameWalk();
|
|
|
|
driver.Collect(
|
|
walk, cell1.CellId, cell1, landscape, ctx, Matrix4x4.Identity, Vector3.Zero);
|
|
driver.Collect(
|
|
walk, cell1.CellId, cell1, landscape, ctx, Matrix4x4.Identity, Vector3.Zero);
|
|
|
|
Console.Out.Flush();
|
|
string[] lines = capture.ToString()
|
|
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(l => l.TrimEnd('\r'))
|
|
.ToArray();
|
|
|
|
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Parse(lines);
|
|
WalkOracleFrame frame = Assert.Single(frames);
|
|
List<WalkOracleEvent> events = frame.Events.ToList();
|
|
|
|
int landscapeIndex = events.FindIndex(e => e.Kind == WalkOracleEventKind.Landscape);
|
|
int firstShellIndex = events.FindIndex(e => e.Kind == WalkOracleEventKind.EnvCellShell);
|
|
Assert.True(landscapeIndex >= 0, "expected an LS line (ov=1 from the fixture's exit portal)");
|
|
Assert.True(firstShellIndex >= 0, "expected the flood's own EC lines");
|
|
Assert.True(
|
|
landscapeIndex < firstShellIndex,
|
|
"the landscape turn must precede the flood's own shells");
|
|
|
|
List<WalkOracleEvent> ocInWindow = events
|
|
.Where((e, i) => e.Kind == WalkOracleEventKind.ObjectCellTurn
|
|
&& i > landscapeIndex && i < firstShellIndex)
|
|
.ToList();
|
|
WalkOracleEvent weatherOc = Assert.Single(ocInWindow);
|
|
Assert.Equal(cell1.CellId, weatherOc.CellId);
|
|
|
|
// The flood's OWN EC/OC pairs (cell1, cell2) still exist, all AFTER
|
|
// the window above — confirms the weather OC is not merely one of
|
|
// those, even though cell1's id happens to match ctx.ViewerCellId.
|
|
List<WalkOracleEvent> floodOc = events
|
|
.Where(e => e.Kind == WalkOracleEventKind.ObjectCellTurn)
|
|
.Skip(1) // drop the weather OC found above
|
|
.ToList();
|
|
Assert.Equal(
|
|
new HashSet<uint> { cell1.CellId, cell2.CellId },
|
|
floodOc.Select(e => e.CellId!.Value).ToHashSet());
|
|
}
|
|
finally
|
|
{
|
|
Console.SetOut(originalOut);
|
|
RenderingDiagnostics.DumpWalkTranscriptEnabled = previous;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Campaign OVERHAUL S3 chunk 4 (O1): the gate's negative half — an
|
|
/// indoor player (or either render toggle off) never prints the weather
|
|
/// "OC" line at all, matching <see
|
|
/// cref="RetailPViewPassExecutor.ShouldDrawWeatherOnce"/>'s own pinned
|
|
/// predicate (<c>RetailPViewPassExecutorTests.
|
|
/// ShouldDrawWeatherOnce_MatchesRetailIsPlayerOutsideGate</c>) — this
|
|
/// test proves the WALK actually honors that gate rather than firing
|
|
/// unconditionally. MUTATION: dropping the <c>if (ctx.WeatherGateOpen)</c>
|
|
/// guard in <see cref="RetailFrameWalk.DrawLandscape"/> would print an
|
|
/// extra "OC" line at the frame's end, making <c>Assert.Same</c> below
|
|
/// see a DIFFERENT cell (the flood's own object turn is what would then
|
|
/// occupy that slot) or the event count assertion fail.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Collect_WeatherGateClosed_NeverFiresTheWeatherTurn()
|
|
{
|
|
bool previous = RenderingDiagnostics.DumpWalkTranscriptEnabled;
|
|
RenderingDiagnostics.DumpWalkTranscriptEnabled = true;
|
|
TextWriter originalOut = Console.Out;
|
|
var capture = new StringWriter();
|
|
try
|
|
{
|
|
Console.SetOut(capture);
|
|
|
|
using var fx = new DispatcherFixture();
|
|
const uint viewerCellId = 0xF4180003u;
|
|
var ctx = new TestContext { WeatherGateOpen = false, ViewerCellId = viewerCellId };
|
|
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
|
|
var leaf = new RecordingLeafRenderer(new List<string>());
|
|
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, new FakeWorldData());
|
|
var walk = new RetailFrameWalk();
|
|
|
|
driver.Collect(
|
|
walk, viewerCellId, null, landscape, ctx, Matrix4x4.Identity, Vector3.Zero);
|
|
driver.Collect(
|
|
walk, viewerCellId, null, landscape, ctx, Matrix4x4.Identity, Vector3.Zero);
|
|
|
|
Console.Out.Flush();
|
|
string[] lines = capture.ToString()
|
|
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(l => l.TrimEnd('\r'))
|
|
.ToArray();
|
|
|
|
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Parse(lines);
|
|
WalkOracleFrame frame = Assert.Single(frames);
|
|
Assert.DoesNotContain(
|
|
frame.Events, e => e.Kind == WalkOracleEventKind.ObjectCellTurn);
|
|
}
|
|
finally
|
|
{
|
|
Console.SetOut(originalOut);
|
|
RenderingDiagnostics.DumpWalkTranscriptEnabled = previous;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Campaign OVERHAUL S3 chunk 4 fix round 1 (K2): proves the print's own
|
|
/// gate and the Replay-time draw's gate are now the SAME predicate —
|
|
/// <see cref="WalkFrameDriver.WeatherTurnFired"/> — instead of two
|
|
/// independently re-derived conditions that could diverge. Before this
|
|
/// fix, the print fired on <c>ctx.WeatherGateOpen</c> alone while the
|
|
/// Replay-time draw additionally required
|
|
/// <c>clipAssembly.OutsideViewSlices.Length > 0</c>, re-derived
|
|
/// separately at Replay — on an interior root whose landscape turn ran
|
|
/// with the gate open but whose reassembled outside-view slices ended
|
|
/// up empty, the transcript could report a weather turn the frame never
|
|
/// actually drew. <see cref="WalkFrameDriver.WeatherTurnFired"/> is exactly the flag
|
|
/// <c>RetailPViewRenderer.DrawLandscapeDynamicsPhase</c> now gates
|
|
/// <c>DrawWeatherOnce</c> on, so this test's flag assertion doubles as
|
|
/// the draw-side pin: gate closed -> the flag stays false (so
|
|
/// <c>DrawWeatherOnce</c> would not fire either) AND no "OC" line
|
|
/// prints; gate open -> the flag becomes true (so
|
|
/// <c>DrawWeatherOnce</c> would fire) AND exactly one "OC" line prints.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public void Collect_WeatherTurnFiredMatchesThePrintedOcLineExactly(bool weatherGateOpen)
|
|
{
|
|
bool previous = RenderingDiagnostics.DumpWalkTranscriptEnabled;
|
|
RenderingDiagnostics.DumpWalkTranscriptEnabled = true;
|
|
TextWriter originalOut = Console.Out;
|
|
var capture = new StringWriter();
|
|
try
|
|
{
|
|
Console.SetOut(capture);
|
|
|
|
using var fx = new DispatcherFixture();
|
|
const uint viewerCellId = 0xF4180003u; // low word < 0x100 -> outdoor root
|
|
var ctx = new TestContext
|
|
{
|
|
WeatherGateOpen = weatherGateOpen,
|
|
ViewerCellId = viewerCellId,
|
|
};
|
|
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
|
|
var leaf = new RecordingLeafRenderer(new List<string>());
|
|
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, new FakeWorldData());
|
|
var walk = new RetailFrameWalk();
|
|
|
|
driver.Collect(
|
|
walk, viewerCellId, null, landscape, ctx, Matrix4x4.Identity, Vector3.Zero);
|
|
|
|
// The one predicate the Replay-time draw now reads instead of
|
|
// re-deriving its own condition — checked before the second
|
|
// Collect call resets it for the next frame.
|
|
Assert.Equal(weatherGateOpen, driver.WeatherTurnFired);
|
|
|
|
// A second Collect call — WalkOracleTrace.Parse (like every
|
|
// real capture) discards the final in-progress frame.
|
|
driver.Collect(
|
|
walk, viewerCellId, null, landscape, ctx, Matrix4x4.Identity, Vector3.Zero);
|
|
|
|
Console.Out.Flush();
|
|
string[] lines = capture.ToString()
|
|
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(l => l.TrimEnd('\r'))
|
|
.ToArray();
|
|
|
|
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Parse(lines);
|
|
WalkOracleFrame frame = Assert.Single(frames);
|
|
bool printed = frame.Events.Any(e => e.Kind == WalkOracleEventKind.ObjectCellTurn);
|
|
Assert.Equal(weatherGateOpen, printed);
|
|
}
|
|
finally
|
|
{
|
|
Console.SetOut(originalOut);
|
|
RenderingDiagnostics.DumpWalkTranscriptEnabled = previous;
|
|
}
|
|
}
|
|
}
|