feat(render): S3 chunk 1 — print-only walk transcript, OH fixtures, offline signature diff
Campaign OVERHAUL S3 chunk 1 (docs/research/2026-09-01-overhaul/s3-walk-ownership-map.md
§11): the transcript-kinds/fixtures/print-only-emitter half of the walk work,
built AFTER chunk 3 landed LC/SC (the per-land-cell interleave).
B1 — the emitter (print-only, never gates admission/depth/order):
- ACDREAM_DUMP_WALK_TRANSCRIPT=1 is read once into RuntimeOptions.DumpWalkTranscript
(rule 4) and handed to RenderingDiagnostics.DumpWalkTranscriptEnabled (rule 5,
a settable static, not a second env read) once at GameWindow construction — the
deep walk call sites have no reachable RuntimeOptions reference.
- WalkTranscriptDump (new) prints the OH line kinds — F/P/LS/LC/SC/BLD/DI/DC/EC/OC —
to Console at the exact points retail's cdb breakpoints sit
(tools/walk-oracle/oh/oh-capture-walk.cdb.template), gated internally so every
method bails out before any string work when the flag is off.
- Every call site lives in WalkFrameDriver.cs, at the point the driver already
processes that turn: Collect (F/P, after BeginFrame), Emit's DI/LS/DC/BLD cases,
OnLandCellTurn/OnLandscapeCellTurn (LC/SC, at LOD resolution via the new
WalkTranscriptDump.LodCellId helper, before the 8x8-bucket expansion), and the
EmitFloodTurns/EmitCellContentsTurn loops (EC/OC — both UNCONDITIONAL per flood
visit, matching the OH captures' always-equal EC/OC counts; retail's own
DrawEnvCell stamp dedupe sits past the breakpoint, inside the function).
- DC's "pv=" field encodes interior(0)/outdoor(1) as an 8-hex-digit 0/1 so it
satisfies the same pv=[0-9a-f]{8} regex real captures use; derived from
_currentDcStage at the DC event (CellStatic = interior pview, else outdoor).
- The frame-root pose (origin, quaternion) is a reasonable orthonormal basis built
from the walk's own CyPlane.Normal forward vector and WalkLandscape's own
ViewerWorldOriginX/Y block origin — self-consistent for the round-trip parser,
not a byte-exact reproduction of retail's Frame (B4's diff never compares P).
B2 — WalkOracleTrace learns EC/OC event kinds (LC/SC already existed from chunk 3).
New WalkOraclePartsTrace.cs holds two small read-only parsers for the parts log
(PD/DM) and the alpha-depth log (AM/FL/PM/PC) — records only, no validator, no
canonical JSONL, no new tool.
B3 — fixtures: the five OH walk captures join WalkOracleTraceTests.AllFixtures
(now (root, name) pairs — FW0's own root plus the OH capture directory) for
parse + complete-frame pins. The four kit poses join WalkTraceConformanceTests'
still-fixture coverage as NEW rows (the OH cathedral-arrival root is f4180108,
not FW0's f4180106): terrace-edge/cathedral-arrival extend the existing theory
(now (root, fixture) parameterized); holtburg-doorway-still and foundry-deep get
dedicated tests mirroring their FW0 siblings' own structure. Finding: the OH
foundry-deep capture's own retail transcript draws 12 real town buildings through
its exit chain (unlike the FW0 capture, which apparently reached none at that
pose) — the FW0 test's stub 1x1 landscape undershoots it (first divergence:
nothing after "LS" vs retail's real BLD content); fixed by building the full
landscape/building world via WalkLandscapeDatBuilder.Build, matching the shared
theory's own approach, not by skipping or weakening the row.
WalkTraceReplayContext.Signature(WalkOracleFrame) now filters to the DI/DC/BLD/LS
kinds (LC/SC/EC/OC never had a WalkEvent analogue in RetailFrameWalk's own
four-kind vocabulary) instead of mapping them to a "?" placeholder, so the still-
fixture comparison stays apples-to-apples on both sides.
B4 — WalkTranscriptSignatureDiff (test-side only, no runner/tool): diffs two
transcripts (raw lines or parsed frames) at the full DI/DC/BLD/LS/LC/SC/EC/OC
level, reporting the first divergent event and position per frame. Proven over a
synthetic self-vs-self-minus-one-event pair (SignatureDiff_ReportsTheExactRemovedEvent).
Tests: T1 (flag off) is split into a unit-level zero-allocation/zero-output check
on WalkTranscriptDump itself (the walk's pre-existing allocation, e.g.
RetailFrameWalk.EmitDrawCells's per-call array, is untouched by this chunk and not
independently zero-alloc) and an integration-level Collect() check; both assert via
WalkOracleTrace.Parse returning zero frames rather than raw string equality, which
is robust to unrelated Console.WriteLine noise from other test classes running in
parallel (a real, observed hazard — WalkFrameDriverTests joins
CameraDiagnosticsCollection for the same reason CornerFloodReplayTests/
Issue181WallPressEquilibriumTests already do, issue #251). T2 proves the
emitter/parser round trip on a synthetic interior frame. T3's InstalledDat rows all
pass. T4: LaunchOptionsDocumentationTests green with the ACDREAM_DUMP_WALK_TRANSCRIPT
row (both directions).
Gates: hermetic lane 6,814/0 (was 6,795 baseline + new tests), three consecutive
clean runs; InstalledDat lane 245/3 known-failures (the two pre-existing #383
layout tests + TowerAscent) unchanged from baseline.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
e813aa1f45
commit
02a8288172
17 changed files with 1267 additions and 22 deletions
|
|
@ -272,6 +272,7 @@ $env:ACDREAM_FRAME_HISTORY = "$scratch\frames.csv"
|
|||
| `ACDREAM_DUMP_GFXOBJS_DIR` | `=<dir>` | overrides the output directory for `ACDREAM_DUMP_GFXOBJS` — Companion output-directory knob for ACDREAM_DUMP_GFXOBJS. | print/file-path only; no effect unless `ACDREAM_DUMP_GFXOBJS` is also set | off/unset | `PhysicsDiagnostics.ProbeDumpGfxObjsPath` |
|
||||
| `ACDREAM_DUMP_SKY` | `=1` | Print-only: dumps decoded `SkyDesc` raw values on region load (`SkyDescLoader.cs`) and per-GfxObj `Surface.Type`/translucency flags on first upload (`SkyRenderer.cs`), plus gates a `TimeSync` console diagnostic in `GameWindow`. Built to resolve specific open questions about retail sky units and GfxObjReplace timing (2026-04-23 research), now answered but the dumps remain wired. — Generic sky-keyframe isolation dump (introduced with the phase-1 tint revert); a tool, not a bug probe. | Three independent reads of the SAME env var, only one of which (`RuntimeOptions.DumpSky`) goes through the typed options object; the other two are raw scattered reads (see Notes). `SkyRenderer.cs:582`'s raw read is in the App layer and has no architectural excuse for bypassing `RuntimeOptions` — `_options.DumpSky` was already available to that composition. `print-only` in all three sites. | off/unset | `RuntimeOptions.DumpSky` (typed) → `GameWindow.cs:704` (`TimeSyncDiagnostic`); **also** two independent raw `Environment.GetEnvironmentVariable` reads at `SkyDescLoader.cs:392` (Core) and `SkyRenderer.cs:582` (App) |
|
||||
| `ACDREAM_DUMP_STEEP_ROOF` | `=1` | gates `[steep-roof] KILL-VELOCITY-APPLIED` in `PhysicsEngine.ResolveWithTransition` when retail's `kill_velocity` zeroes body velocity on steep-slope impact, plus per-frame plane-normal traces in `TransitionTypes`/`PlayerMovementController` — KEEP: observes LIVE divergence-register row AD-56 (the plumb-fall freeze on steep-but-walkable polys, restored 2026-08-07). The only runtime lens on that active divergence; delete only with the AD-56 row itself. | print-only | off/unset | `PhysicsDiagnostics.DumpSteepRoofEnabled` |
|
||||
| `ACDREAM_DUMP_WALK_TRANSCRIPT` | `=1` | Campaign OVERHAUL S3 chunk 1 (§11.2 B1): the production frame walk (`RetailFrameWalk` + `WalkFrameDriver`) prints the OH oracle-trace line kinds — `F`/`P`/`LS`/`LC`/`SC`/`BLD`/`DI`/`DC`/`EC`/`OC` — to `Console.Out` at the exact points retail's cdb breakpoints sit (`tools/walk-oracle/oh/oh-capture-walk.cdb.template`), so a Release run's own transcript can be diffed offline against a live retail capture (frame-by-frame, at the DI/DC/BLD/LS/LC/SC/EC/OC level — the test-side `WalkTranscriptSignatureDiff` helper does the diff, not this flag). | print-only; every print sits AFTER the walk has already decided to emit the corresponding turn — never gates admission, depth state, or draw order. Off, the emitter (`WalkTranscriptDump`) does no string work and allocates nothing (`WalkFrameDriverTests.TranscriptEmitter_FlagOff_EveryPrintMethodIsAZeroCostNoOp`). | off/unset | `RuntimeOptions.DumpWalkTranscript` (typed, sole env read) → `GameWindow.cs` (one-time handoff at construction) → `RenderingDiagnostics.DumpWalkTranscriptEnabled` (the actual gate every call site checks) → `WalkTranscriptDump` / `WalkFrameDriver.cs` |
|
||||
| `ACDREAM_HIDE_PART` | `=<int>` | Hides one mesh part by index on entities with ≥10 parts (humanoids) — a debugging aid for equipment/clothing part-visibility issues. — Generic model-part isolation tool (issue #37 lineage but general-purpose since); a tool, not a bug probe. | Real (visible) behavior change, not print-only, but scoped to a single diagnostic index and off by default. | off/unset | `RuntimeOptions.HidePartIndex` → `LivePresentationComposition.cs:608` → `LiveEntityAnimationPresenter.cs:21,38,243` |
|
||||
| `ACDREAM_PROBE_CELL` | `=1` | gates one `[cell-transit]` line per `PlayerMovementController.CellId` change (old→new cell, position, reason tag) — Standing cell-transit tracer (L.2a slice 1), pair of the permanent ACDREAM_PROBE_RESOLVE; recurs in every membership investigation. | print-only; low volume (only on actual cell crossings) | off/unset | `PhysicsDiagnostics.ProbeCellEnabled` |
|
||||
|
||||
|
|
|
|||
|
|
@ -693,6 +693,13 @@ public sealed class GameWindow :
|
|||
AcDream.App.Plugins.BufferedRenderPackRegistry? renderPackRegistry = null)
|
||||
{
|
||||
_options = options ?? throw new System.ArgumentNullException(nameof(options));
|
||||
// Campaign OVERHAUL S3 chunk 1 (§11.2 B1): the one-time RuntimeOptions
|
||||
// (rule 4) → RenderingDiagnostics (rule 5) handoff for the print-only
|
||||
// walk-transcript emitter — see RuntimeOptions.DumpWalkTranscript's
|
||||
// own doc comment for why the deep walk call sites read the static
|
||||
// property instead of this typed option directly.
|
||||
AcDream.Core.Rendering.RenderingDiagnostics.DumpWalkTranscriptEnabled =
|
||||
options.DumpWalkTranscript;
|
||||
_automation = automation;
|
||||
_statusWriter = new SessionStatusWriter(options.StatusFilePath);
|
||||
_platformServices = platformServices
|
||||
|
|
|
|||
|
|
@ -713,6 +713,13 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
private int _cellViewRouteIndex;
|
||||
private int _landscapeViewRouteIndex;
|
||||
|
||||
/// <summary>S3 chunk 1 (§11.2 B1): the "F n" transcript frame counter —
|
||||
/// monotonic across this driver's whole lifetime (never reset by
|
||||
/// BeginFrame/AbortFrame/EndFrame), matching a live cdb capture's own
|
||||
/// per-process frame counter. Only ever incremented when the transcript
|
||||
/// flag is on, so it costs nothing while off.</summary>
|
||||
private int _transcriptFrameNumber;
|
||||
|
||||
internal WalkFrameDriver(
|
||||
WbDrawDispatcher dispatcher,
|
||||
IWalkFrameLeafRenderer leafRenderer,
|
||||
|
|
@ -849,6 +856,23 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
ArgumentNullException.ThrowIfNull(ctx);
|
||||
|
||||
BeginFrame(ctx, viewProjection, cameraWorldPosition);
|
||||
// S3 chunk 1 (§11.2 B1): the frame-root "F n" + "P …" print — AFTER
|
||||
// BeginFrame (a scratch reset, not an admission decision), BEFORE
|
||||
// the walk itself runs, matching retail's SmartBox::RenderNormalMode
|
||||
// @0x00453aa0 breakpoint (entered once per frame, before either the
|
||||
// outdoor or the indoor branch). Every cost here (the counter
|
||||
// increment, the landblock-local subtraction) sits inside the flag
|
||||
// check, so an ordinary launch pays nothing.
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.DumpWalkTranscriptEnabled)
|
||||
{
|
||||
_transcriptFrameNumber++;
|
||||
WalkTranscriptDump.PrintFrameRoot(
|
||||
_transcriptFrameNumber,
|
||||
cameraCellId,
|
||||
cameraWorldPosition - new Vector3(
|
||||
landscape.ViewerWorldOriginX, landscape.ViewerWorldOriginY, 0f),
|
||||
ctx.CyPlane.Normal);
|
||||
}
|
||||
try
|
||||
{
|
||||
walk.WalkFrame(cameraCellId, cameraCell, landscape, ctx, this);
|
||||
|
|
@ -1295,18 +1319,43 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
switch (walkEvent.Kind)
|
||||
{
|
||||
case WalkEventKind.DrawInside:
|
||||
// S3 chunk 1 (§11.2 B1): PView::DrawInside @0x005a5860 entry.
|
||||
WalkTranscriptDump.PrintDrawInside(walkEvent.CellId);
|
||||
_currentDcStage = WalkDrawStage.CellStatic;
|
||||
VisitedCells.Add(walkEvent.CellId);
|
||||
break;
|
||||
case WalkEventKind.Landscape:
|
||||
// S3 chunk 1 (§11.2 B1): LScape::draw @0x00506330 entry —
|
||||
// printed before the driver's own duplicate-turn guard so a
|
||||
// desynced second LS still shows up in the transcript
|
||||
// (the guard is a walk/driver fail-loud check, not a retail
|
||||
// admission decision retail's own breakpoint would skip).
|
||||
WalkTranscriptDump.PrintLandscape();
|
||||
HandleLandscapeTurn(walkEvent.OutsideViewCount);
|
||||
break;
|
||||
case WalkEventKind.DrawCells:
|
||||
// S3 chunk 1 (§11.2 B1): PView::DrawCells @0x005a4840 entry
|
||||
// (breakpoint-entry order, matching the FW0/OH captures).
|
||||
// _currentDcStage at THIS point (set by the immediately
|
||||
// preceding DI or OnBuildingTurn event, never mutated by
|
||||
// this case itself) tells us which PView owns this flood:
|
||||
// CellStatic == the interior root's own flood (interior
|
||||
// pview); anything else (LookInStatic) == a building
|
||||
// look-in through the outdoor pview.
|
||||
WalkTranscriptDump.PrintDrawCells(
|
||||
outdoorPview: _currentDcStage == WalkDrawStage.LookInStatic,
|
||||
walkEvent.OutsideViewCount,
|
||||
walkEvent.Cells);
|
||||
foreach (uint id in walkEvent.Cells)
|
||||
VisitedCells.Add(id);
|
||||
HandleDrawCellsTurn(walkEvent.Cells);
|
||||
break;
|
||||
case WalkEventKind.Building:
|
||||
// S3 chunk 1 (§11.2 B1): RenderDeviceD3D::DrawBuilding
|
||||
// @0x0059f2a0 entry — unconditional, matching the BLD event's
|
||||
// own "fires at ENTRY, before the degrade check" contract
|
||||
// (this type's class doc).
|
||||
WalkTranscriptDump.PrintBuilding(walkEvent.CellId);
|
||||
// OnBuildingTurn (below) carries the actual side effects —
|
||||
// this vocabulary-only event needs no driver action.
|
||||
break;
|
||||
|
|
@ -1331,6 +1380,13 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
if ((uint)cellIndex >= (uint)(sideCellCount * sideCellCount))
|
||||
throw new ArgumentOutOfRangeException(nameof(cellIndex));
|
||||
|
||||
// S3 chunk 1 (§11.2 B1): RenderDeviceD3D::DrawLandCell @0x0059f120
|
||||
// entry — one LC line per admitted LOD land cell, at LOD resolution
|
||||
// (never expanded to the 8x8 owner-bucket ids OnLandscapeCellTurn
|
||||
// uses for object-list content).
|
||||
WalkTranscriptDump.PrintLandCell(
|
||||
WalkTranscriptDump.LodCellId(landblockId, sideCellCount, cellIndex));
|
||||
|
||||
// The one mark rule (this type's own doc comment): flush any
|
||||
// already-queued stream content ahead of this leaf-renderer event.
|
||||
MarkIfGrown();
|
||||
|
|
@ -1355,6 +1411,13 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
if ((uint)cellIndex >= (uint)(sideCellCount * sideCellCount))
|
||||
throw new ArgumentOutOfRangeException(nameof(cellIndex));
|
||||
|
||||
// S3 chunk 1 (§11.2 B1): RenderDeviceD3D::DrawSortCell @0x0059f140
|
||||
// entry — ONE SC line per LOD cell (printed before the 8x8-bucket
|
||||
// expansion below, which is an acdream object-list-content detail
|
||||
// with no retail-breakpoint analogue of its own).
|
||||
WalkTranscriptDump.PrintSortCell(
|
||||
WalkTranscriptDump.LodCellId(landblockId, sideCellCount, cellIndex));
|
||||
|
||||
uint blockPrefix = landblockId & 0xFFFF0000u;
|
||||
if (sideCellCount == 8)
|
||||
{
|
||||
|
|
@ -1675,6 +1738,17 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
{
|
||||
int viewRouteIndex = CaptureCellViewRoute(cells[i]);
|
||||
_floodViewRouteScratch[i] = viewRouteIndex;
|
||||
// S3 chunk 1 (§11.2 B1): RenderDeviceD3D::DrawEnvCell
|
||||
// @0x0059f170 entry — UNCONDITIONAL, before the per-render-stamp
|
||||
// dedupe below. Retail's own stamp check (GetDrawnThisFrame/
|
||||
// SetDrawnThisFrame) sits INSIDE DrawEnvCell, past this
|
||||
// breakpoint's address, so every flood visit prints an EC line
|
||||
// even when the visit's actual shell submission is suppressed —
|
||||
// the OH captures' EC and OC counts are always exactly equal per
|
||||
// pose (e.g. 45/45 at holtburg-doorway-still), which only holds
|
||||
// if EC is visit-scoped like OC, not stamp-deduped like the
|
||||
// WalkFrameEvent.CellShell submission below.
|
||||
WalkTranscriptDump.PrintEnvCellShell(cells[i]);
|
||||
if (_cellShellsDrawnThisFrame.Add(cells[i]))
|
||||
{
|
||||
bool skipSouthLookInShell =
|
||||
|
|
@ -1712,6 +1786,14 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
uint cellId,
|
||||
int viewRouteIndex)
|
||||
{
|
||||
// S3 chunk 1 (§11.2 B1): RenderDeviceD3D::DrawObjCellForDummies
|
||||
// @0x005a0760 entry — the interior/look-in object-list turn (retail's
|
||||
// outdoor DrawObjCell is a different function, folded into the SC
|
||||
// line's own DrawSortCell call). Unconditional, matching this loop's
|
||||
// existing no-dedupe shape (every flood visit already calls this
|
||||
// method once, with no per-stamp gate).
|
||||
WalkTranscriptDump.PrintObjectCellTurn(cellId);
|
||||
|
||||
// The shell loop captured the exact live portal_view for this cell.
|
||||
// Static and dynamic object lists reuse that same route: retail's
|
||||
// DrawEnvCell and DrawObjCell consume the same PortalList.
|
||||
|
|
|
|||
191
src/AcDream.App/Rendering/Walk/WalkTranscriptDump.cs
Normal file
191
src/AcDream.App/Rendering/Walk/WalkTranscriptDump.cs
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
||||
namespace AcDream.App.Rendering.Walk;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OVERHAUL S3 chunk 1 (§11.2 B1): the print-only walk-transcript
|
||||
/// emitter. Prints the OH oracle-trace line kinds — <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> — to <see cref="Console"/> at the EXACT points retail's cdb
|
||||
/// breakpoints sit
|
||||
/// (<c>tools/walk-oracle/oh/oh-capture-walk.cdb.template</c>), so a Release
|
||||
/// run's output can be diffed offline against a live retail capture (§11.2
|
||||
/// B4). Gated by
|
||||
/// <see cref="AcDream.Core.Rendering.RenderingDiagnostics.DumpWalkTranscriptEnabled"/>
|
||||
/// (rule 5) — every method here returns immediately, before any string
|
||||
/// work, when the flag is off (T1: zero output, zero extra allocation on
|
||||
/// the walk path).
|
||||
///
|
||||
/// <para>Every hex value prints lowercase, 8 digits
|
||||
/// (<c>value.ToString("x8")</c>) to match <c>WalkOracleTrace</c>'s
|
||||
/// <c>[0-9a-f]{8}</c> regex family — an uppercase or short-padded print
|
||||
/// silently fails to parse.</para>
|
||||
///
|
||||
/// <para>This class never influences admission, depth state, or draw
|
||||
/// order — every call site in <see cref="WalkFrameDriver"/> invokes it
|
||||
/// AFTER the walk has already decided to emit the corresponding turn.</para>
|
||||
/// </summary>
|
||||
internal static class WalkTranscriptDump
|
||||
{
|
||||
private static bool Enabled =>
|
||||
AcDream.Core.Rendering.RenderingDiagnostics.DumpWalkTranscriptEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// The frame-root pair — "F n" then "P …" — printed once per
|
||||
/// <see cref="WalkFrameDriver.Collect"/> call (retail
|
||||
/// <c>SmartBox::RenderNormalMode</c> @0x00453aa0).
|
||||
/// <paramref name="forward"/> is the walk's own CY-plane normal
|
||||
/// (<c>ctx.CyPlane.Normal</c>) — the SAME forward direction the walk
|
||||
/// already derived for this frame (<c>WalkProductionFrameContext</c>/
|
||||
/// <c>WalkTraceReplayContext</c> both store it there), so this costs no
|
||||
/// extra camera-state lookup. The printed quaternion is a reasonable
|
||||
/// orthonormal basis built FROM that forward vector (right = forward ×
|
||||
/// worldUp, up = right × forward) in retail's w,x,y,z storage order — an
|
||||
/// internally self-consistent pose for the round-trip parser (T2), not a
|
||||
/// byte-exact reproduction of retail's own internal Frame (the B4
|
||||
/// signature diff never compares the P line — see
|
||||
/// <c>WalkTraceReplayContext.Signature</c>'s DI/DC/BLD/LS/LC/SC/EC/OC
|
||||
/// scope). <paramref name="landblockLocalOrigin"/> is precomputed by the
|
||||
/// caller from <c>WalkLandscape.ViewerWorldOriginX/Y</c> — the SAME 192 m
|
||||
/// block origin the walk's own terrain/scenery placement already uses —
|
||||
/// so this method does no coordinate math of its own.
|
||||
/// </summary>
|
||||
internal static void PrintFrameRoot(
|
||||
int frameNumber,
|
||||
uint cameraCellId,
|
||||
Vector3 landblockLocalOrigin,
|
||||
Vector3 forward)
|
||||
{
|
||||
if (!Enabled) return;
|
||||
|
||||
// A forward nearly parallel to world-up degenerates the usual
|
||||
// forward×worldUp cross product (an outdoor root looking straight
|
||||
// down, or a synthetic test fixture); fall back to a different
|
||||
// reference axis rather than emit a NaN/zero-length basis.
|
||||
Vector3 worldUp = MathF.Abs(Vector3.Dot(forward, Vector3.UnitZ)) > 0.999f
|
||||
? Vector3.UnitY
|
||||
: Vector3.UnitZ;
|
||||
Vector3 right = Vector3.Normalize(Vector3.Cross(forward, worldUp));
|
||||
Vector3 up = Vector3.Cross(right, forward);
|
||||
// Row-vector convention (System.Numerics: v' = v * M, so
|
||||
// Vector3.Transform(axis, M) reads out ROW axis of M): row1→right
|
||||
// (UnitX), row2→forward (UnitY), row3→up (UnitZ) — the SAME
|
||||
// convention WalkTraceReplayContext decodes a P line with
|
||||
// (forward = Transform(UnitY, rotation), up = Transform(UnitZ, …),
|
||||
// right = Transform(UnitX, …)), so a self-produced P line round-
|
||||
// trips through that SAME decoding, not just through the parser.
|
||||
var basis = new Matrix4x4(
|
||||
right.X, right.Y, right.Z, 0f,
|
||||
forward.X, forward.Y, forward.Z, 0f,
|
||||
up.X, up.Y, up.Z, 0f,
|
||||
0f, 0f, 0f, 1f);
|
||||
Quaternion q = Quaternion.CreateFromRotationMatrix(basis);
|
||||
|
||||
Console.WriteLine($"F {frameNumber}");
|
||||
Console.WriteLine(
|
||||
$"P {cameraCellId.ToString("x8")} "
|
||||
+ $"{HexOf(landblockLocalOrigin.X)} {HexOf(landblockLocalOrigin.Y)} {HexOf(landblockLocalOrigin.Z)} "
|
||||
+ $"{HexOf(q.W)} {HexOf(q.X)} {HexOf(q.Y)} {HexOf(q.Z)}");
|
||||
}
|
||||
|
||||
/// <summary><c>LScape::draw</c> @0x00506330 entry.</summary>
|
||||
internal static void PrintLandscape()
|
||||
{
|
||||
if (!Enabled) return;
|
||||
Console.WriteLine("LS");
|
||||
}
|
||||
|
||||
/// <summary><c>RenderDeviceD3D::DrawBuilding</c> @0x0059f2a0 entry.</summary>
|
||||
internal static void PrintBuilding(uint positionCellId)
|
||||
{
|
||||
if (!Enabled) return;
|
||||
Console.WriteLine($"BLD {positionCellId.ToString("x8")}");
|
||||
}
|
||||
|
||||
/// <summary><c>PView::DrawInside</c> @0x005a5860 entry.</summary>
|
||||
internal static void PrintDrawInside(uint cellId)
|
||||
{
|
||||
if (!Enabled) return;
|
||||
Console.WriteLine($"DI {cellId.ToString("x8")}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>PView::DrawCells</c> @0x005a4840 entry. <paramref name="outdoorPview"/>
|
||||
/// is false for the interior pview's own root flood, true for a
|
||||
/// building look-in through the outdoor pview (the caller derives this
|
||||
/// from which stage is active at the DC event — see
|
||||
/// <c>WalkFrameDriver.Emit</c>'s <c>DrawCells</c> case). The "pv=" field
|
||||
/// prints as an 8-hex-digit 0/1 (<c>00000000</c>/<c>00000001</c>)
|
||||
/// rather than a real pointer so it satisfies the SAME
|
||||
/// <c>pv=[0-9a-f]{8}</c> regex the retail captures use — the parser
|
||||
/// ignores its value either way (§11.2 B1).
|
||||
/// </summary>
|
||||
internal static void PrintDrawCells(
|
||||
bool outdoorPview, int outsideViewCount, IReadOnlyList<uint> cells)
|
||||
{
|
||||
if (!Enabled) return;
|
||||
var sb = new StringBuilder(48 + cells.Count * 9);
|
||||
sb.Append("DC pv=").Append(outdoorPview ? "00000001" : "00000000");
|
||||
sb.Append(" ov=").Append(outsideViewCount);
|
||||
sb.Append(" n=").Append(cells.Count).Append(':');
|
||||
for (int i = 0; i < cells.Count; i++)
|
||||
sb.Append(' ').Append(cells[i].ToString("x8"));
|
||||
Console.WriteLine(sb.ToString());
|
||||
}
|
||||
|
||||
/// <summary><c>RenderDeviceD3D::DrawLandCell</c> @0x0059f120 entry.</summary>
|
||||
internal static void PrintLandCell(uint cellId)
|
||||
{
|
||||
if (!Enabled) return;
|
||||
Console.WriteLine($"LC {cellId.ToString("x8")}");
|
||||
}
|
||||
|
||||
/// <summary><c>RenderDeviceD3D::DrawSortCell</c> @0x0059f140 entry.</summary>
|
||||
internal static void PrintSortCell(uint cellId)
|
||||
{
|
||||
if (!Enabled) return;
|
||||
Console.WriteLine($"SC {cellId.ToString("x8")}");
|
||||
}
|
||||
|
||||
/// <summary><c>RenderDeviceD3D::DrawEnvCell</c> @0x0059f170 entry — fires
|
||||
/// once per (flood, cell) VISIT, not once per render stamp: the retail
|
||||
/// captures' EC and OC counts are always exactly equal per pose (e.g.
|
||||
/// 45/45 at holtburg-doorway-still), which only holds if EC — like OC —
|
||||
/// is unconditional at every flood visit, with the per-stamp dedupe
|
||||
/// happening INSIDE DrawEnvCell, after this breakpoint's address, not in
|
||||
/// the calling loop.</summary>
|
||||
internal static void PrintEnvCellShell(uint cellId)
|
||||
{
|
||||
if (!Enabled) return;
|
||||
Console.WriteLine($"EC {cellId.ToString("x8")}");
|
||||
}
|
||||
|
||||
/// <summary><c>RenderDeviceD3D::DrawObjCellForDummies</c> @0x005a0760
|
||||
/// entry — the interior/look-in object-list turn (retail's outdoor
|
||||
/// <c>DrawObjCell</c> is a different function, folded into the "SC"
|
||||
/// line's own DrawSortCell call; it has no separate OC hook).</summary>
|
||||
internal static void PrintObjectCellTurn(uint cellId)
|
||||
{
|
||||
if (!Enabled) return;
|
||||
Console.WriteLine($"OC {cellId.ToString("x8")}");
|
||||
}
|
||||
|
||||
/// <summary>S3 §9.1 R2's LOD cell id: <c>x*8+y+1</c> in LOD coordinates
|
||||
/// (a side-2 block's cells are 1, 2, 9, 10), composed onto the
|
||||
/// landblock prefix — the SAME id
|
||||
/// <c>WalkFrameDriver</c>'s <c>OnLandscapeCellTurn(landblockId,8,cellIndex)</c>
|
||||
/// fast path already produces for side==8, generalized to every LOD.
|
||||
/// <paramref name="sideCellCount"/>/<paramref name="cellIndex"/> travel
|
||||
/// PACKED (LOD resolution), matching <c>OnLandCellTurn</c>'s own
|
||||
/// contract — never the expanded 8×8-bucket resolution.</summary>
|
||||
internal static uint LodCellId(uint landblockId, int sideCellCount, int cellIndex)
|
||||
{
|
||||
int x = cellIndex / sideCellCount;
|
||||
int y = cellIndex % sideCellCount;
|
||||
return (landblockId & 0xFFFF0000u) | checked((uint)(x * 8 + y + 1));
|
||||
}
|
||||
|
||||
private static string HexOf(float value)
|
||||
=> unchecked((uint)BitConverter.SingleToInt32Bits(value)).ToString("x8");
|
||||
}
|
||||
|
|
@ -48,6 +48,15 @@ public sealed record RuntimeOptions(
|
|||
/// production input produced outbound movement traffic.</summary>
|
||||
bool DumpMoveTruth,
|
||||
bool DumpSky,
|
||||
/// <summary>Campaign OVERHAUL S3 chunk 1 (§11.2 B1): the sole
|
||||
/// authoritative read of <c>ACDREAM_DUMP_WALK_TRANSCRIPT</c> (rule 4).
|
||||
/// <c>GameWindow</c>'s constructor assigns this into
|
||||
/// <c>RenderingDiagnostics.DumpWalkTranscriptEnabled</c> (rule 5) once
|
||||
/// at startup — the deep walk call sites (<c>WalkFrameDriver</c>,
|
||||
/// <c>WalkTranscriptDump</c>) check that static property, not this
|
||||
/// field, since they have no reachable <see cref="RuntimeOptions"/>
|
||||
/// reference.</summary>
|
||||
bool DumpWalkTranscript,
|
||||
bool NoAudio,
|
||||
int HidePartIndex,
|
||||
bool RetailCloseDegrades,
|
||||
|
|
@ -163,6 +172,7 @@ public sealed record RuntimeOptions(
|
|||
UncappedRendering: IsExactlyOne(env("ACDREAM_UNCAPPED_RENDER")),
|
||||
DumpMoveTruth: IsExactlyOne(env("ACDREAM_DUMP_MOVE_TRUTH")),
|
||||
DumpSky: IsExactlyOne(env("ACDREAM_DUMP_SKY")),
|
||||
DumpWalkTranscript: IsExactlyOne(env("ACDREAM_DUMP_WALK_TRANSCRIPT")),
|
||||
NoAudio: IsExactlyOne(env("ACDREAM_NO_AUDIO")),
|
||||
HidePartIndex: TryParseInt(env("ACDREAM_HIDE_PART")) ?? -1,
|
||||
// Default-on: any value other than the literal string "0" enables
|
||||
|
|
|
|||
|
|
@ -670,4 +670,30 @@ public static class RenderingDiagnostics
|
|||
/// </summary>
|
||||
public static string? FrameHistoryPath { get; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_FRAME_HISTORY");
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OVERHAUL S3 chunk 1 (§11.2 B1): print-only walk-transcript
|
||||
/// emitter. When true, the production frame walk
|
||||
/// (<c>RetailFrameWalk</c> + <c>WalkFrameDriver</c>,
|
||||
/// <c>src/AcDream.App/Rendering/Walk/WalkTranscriptDump.cs</c>) prints
|
||||
/// the OH oracle-trace line kinds — F/P/LS/LC/SC/BLD/DI/DC/EC/OC — to
|
||||
/// Console at the exact points retail's cdb breakpoints sit
|
||||
/// (<c>tools/walk-oracle/oh/oh-capture-walk.cdb.template</c>). Print-
|
||||
/// only: it never gates admission, depth state, or draw order — every
|
||||
/// print call sits AFTER the walk has already decided to emit the
|
||||
/// corresponding turn, and every print call bails out before any string
|
||||
/// work when this is false.
|
||||
/// <para>
|
||||
/// This is the "owned by RenderingDiagnostics (rule 5)" half of §11.2
|
||||
/// B1's design; the sole authoritative read of
|
||||
/// <c>ACDREAM_DUMP_WALK_TRANSCRIPT</c> lives in
|
||||
/// <c>RuntimeOptions.DumpWalkTranscript</c> (rule 4) — this property
|
||||
/// defaults false and is set exactly once, at <c>GameWindow</c>
|
||||
/// construction, from that typed option (never reads the environment
|
||||
/// directly itself, unlike this file's other flags), so the deep walk
|
||||
/// call sites that have no reachable <c>RuntimeOptions</c> reference
|
||||
/// still get one static, flippable-at-runtime gate to check.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static bool DumpWalkTranscriptEnabled { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,8 +31,12 @@ namespace AcDream.App.Tests.Rendering;
|
|||
/// <item><description><c>AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled</c>
|
||||
/// — written by <c>CornerFloodReplayTests</c> and
|
||||
/// <c>Issue181WallPressEquilibriumTests</c>.</description></item>
|
||||
/// <item><description><c>AcDream.Core.Rendering.RenderingDiagnostics.DumpWalkTranscriptEnabled</c>
|
||||
/// (Campaign OVERHAUL S3 chunk 1) — written by
|
||||
/// <c>WalkFrameDriverTests</c>' transcript-emitter tests
|
||||
/// (<c>WalkFrameDriverTranscriptTests.cs</c>).</description></item>
|
||||
/// <item><description><c>System.Console.Out</c> — redirected via
|
||||
/// <c>Console.SetOut</c> by those same two classes to capture probe output.
|
||||
/// <c>Console.SetOut</c> by those same classes to capture probe output.
|
||||
/// Interleaved redirection can restore a DISPOSED <c>StringWriter</c> as the
|
||||
/// process-wide <c>Console.Out</c>, which then throws in unrelated
|
||||
/// tests.</description></item>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ namespace AcDream.App.Tests.Rendering.Walk;
|
|||
/// still does not construct this driver); every world-data/leaf-renderer
|
||||
/// dependency here is a synthetic fake per plan §FW3.2b-1.
|
||||
/// </summary>
|
||||
public sealed class WalkFrameDriverTests
|
||||
public sealed partial class WalkFrameDriverTests
|
||||
{
|
||||
// ── Shared ordered log: BOTH the fake leaf renderer and the fake trace
|
||||
// write into ONE list, so a single sequence assertion proves the FULL
|
||||
|
|
|
|||
|
|
@ -0,0 +1,344 @@
|
|||
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.</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);
|
||||
|
||||
AssertNoTranscriptLines(capture);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(originalOut);
|
||||
RenderingDiagnostics.DumpWalkTranscriptEnabled = previous;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// </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));
|
||||
}
|
||||
|
||||
/// <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 (WalkTranscriptDump.PrintEnvCellShell's own doc
|
||||
// comment: EC/OC counts are always exactly equal per pose).
|
||||
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 for
|
||||
// those three kinds is proven 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>§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;
|
||||
}
|
||||
}
|
||||
}
|
||||
273
tests/AcDream.App.Tests/Rendering/Walk/WalkOraclePartsTrace.cs
Normal file
273
tests/AcDream.App.Tests/Rendering/Walk/WalkOraclePartsTrace.cs
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering.Walk;
|
||||
|
||||
/// <summary>
|
||||
/// S3 chunk 1 (§11.2 B2): small, read-only parsers for the OH capture
|
||||
/// family's two companion logs — the PARTS log
|
||||
/// (<c>tools/walk-oracle/oh/oh-capture-parts.cdb.template</c>: PD/DM) and
|
||||
/// the ALPHA-DEPTH log
|
||||
/// (<c>tools/walk-oracle/oh/oh-capture-alpha-depth.cdb.template</c>: AM/FL/
|
||||
/// PM/PC), both dumped from the SAME <c>docs/research/2026-09-01-overhaul/
|
||||
/// oh-capture/</c> directory as the walk log <see cref="WalkOracleTrace"/>
|
||||
/// already parses. These are records only — no validator, no canonical
|
||||
/// JSONL, no new tool ([[feedback-evidence-infrastructure-sink]]) — a later
|
||||
/// chunk/S4 names the first real consumer and pins specific values then.
|
||||
/// </summary>
|
||||
public static class WalkOraclePartsTrace
|
||||
{
|
||||
/// <summary>Parses a PARTS log (<c><pose>.parts.log</c>). Same
|
||||
/// F/P framing and truncated-last-frame drop rule as
|
||||
/// <see cref="WalkOracleTrace.Parse"/> — the harness detaches at the
|
||||
/// frame marker, so the final "F n" never records its own PD/DM lines.</summary>
|
||||
public static IReadOnlyList<WalkOraclePartsFrame> Parse(IEnumerable<string> lines)
|
||||
{
|
||||
var frames = new List<WalkOraclePartsFrame>();
|
||||
List<WalkOraclePartDraw>? partDraws = null;
|
||||
List<WalkOracleMeshDraw>? meshDraws = null;
|
||||
int currentNumber = 0;
|
||||
|
||||
foreach (string line in lines)
|
||||
{
|
||||
Match frameMatch = FramePattern.Match(line);
|
||||
if (frameMatch.Success)
|
||||
{
|
||||
if (partDraws is not null)
|
||||
{
|
||||
frames.Add(new WalkOraclePartsFrame(currentNumber, partDraws, meshDraws!));
|
||||
}
|
||||
currentNumber = int.Parse(
|
||||
frameMatch.Groups[1].Value, CultureInfo.InvariantCulture);
|
||||
partDraws = new List<WalkOraclePartDraw>();
|
||||
meshDraws = new List<WalkOracleMeshDraw>();
|
||||
continue;
|
||||
}
|
||||
if (partDraws is null)
|
||||
continue;
|
||||
|
||||
Match pd = PartDrawPattern.Match(line);
|
||||
if (pd.Success)
|
||||
{
|
||||
partDraws.Add(new WalkOraclePartDraw(
|
||||
ParseHex(pd.Groups[1].Value),
|
||||
ParseHex(pd.Groups[2].Value),
|
||||
pd.Groups[3].Value != "0",
|
||||
ParseHex(pd.Groups[4].Value)));
|
||||
continue;
|
||||
}
|
||||
Match dm = MeshDrawPattern.Match(line);
|
||||
if (dm.Success)
|
||||
{
|
||||
meshDraws!.Add(new WalkOracleMeshDraw(
|
||||
ParseHex(dm.Groups[1].Value),
|
||||
ParseHex(dm.Groups[2].Value),
|
||||
dm.Groups[3].Value != "0",
|
||||
int.Parse(dm.Groups[4].Value, CultureInfo.InvariantCulture),
|
||||
ParseHex(dm.Groups[5].Value)));
|
||||
continue;
|
||||
}
|
||||
// Anything else (the "P …" pose line included — parts fixtures
|
||||
// carry no pose consumer today) is ignored, matching
|
||||
// WalkOracleTrace's own "cdb chrome" tolerance.
|
||||
}
|
||||
|
||||
return frames;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<WalkOraclePartsFrame> Load(string root, string fixtureName)
|
||||
=> Parse(File.ReadLines(FixturePath(root, fixtureName, ".parts.log")));
|
||||
|
||||
private static string FixturePath(string root, string fixtureName, string suffix)
|
||||
{
|
||||
string repoRoot = WalkOracleTraceRepoRoot.Find();
|
||||
return Path.Combine(
|
||||
repoRoot, Path.Combine(root.Split('/')), fixtureName + suffix);
|
||||
}
|
||||
|
||||
private static uint ParseHex(string hex)
|
||||
=> uint.Parse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
|
||||
|
||||
private static readonly Regex FramePattern = new(@"^F (\d+)\s*$", RegexOptions.Compiled);
|
||||
|
||||
// PD gfx=<hex8> did=<hex8> force=<0|1> cell=<hex8>
|
||||
private static readonly Regex PartDrawPattern = new(
|
||||
@"^PD gfx=([0-9a-f]{8}) did=([0-9a-f]{8}) force=(\d) cell=([0-9a-f]{8})\s*$",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
// DM gfx=<hex8> did=<hex8> force=<0|1> bound=<0|1|2> cell=<hex8>
|
||||
private static readonly Regex MeshDrawPattern = new(
|
||||
@"^DM gfx=([0-9a-f]{8}) did=([0-9a-f]{8}) force=(\d) bound=(\d) cell=([0-9a-f]{8})\s*$",
|
||||
RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
/// <summary>One <c>CPhysicsPart::Draw</c> @0x0050D7A0 entry (PD line).</summary>
|
||||
public sealed record WalkOraclePartDraw(uint Gfx, uint DataId, bool Force, uint Cell);
|
||||
|
||||
/// <summary>One <c>RenderDeviceD3D::DrawMeshInternal</c> @0x0059F360 entry
|
||||
/// (DM line). <paramref name="Bound"/> is retail's BoundingType (0=OUTSIDE,
|
||||
/// 1=PARTIALLY_INSIDE, 2=ENTIRELY_INSIDE).</summary>
|
||||
public sealed record WalkOracleMeshDraw(uint Gfx, uint DataId, bool Force, int Bound, uint Cell);
|
||||
|
||||
public sealed record WalkOraclePartsFrame(
|
||||
int Number,
|
||||
IReadOnlyList<WalkOraclePartDraw> PartDraws,
|
||||
IReadOnlyList<WalkOracleMeshDraw> MeshDraws);
|
||||
|
||||
/// <summary>
|
||||
/// S3 chunk 1 (§11.2 B2): parses an ALPHA-DEPTH log
|
||||
/// (<c><pose>.alphadepth.log</c>). Same F/P framing and
|
||||
/// truncated-last-frame drop rule as <see cref="WalkOracleTrace.Parse"/>.
|
||||
/// </summary>
|
||||
public static class WalkOracleAlphaDepthTrace
|
||||
{
|
||||
public static IReadOnlyList<WalkOracleAlphaDepthFrame> Parse(IEnumerable<string> lines)
|
||||
{
|
||||
var frames = new List<WalkOracleAlphaDepthFrame>();
|
||||
List<WalkOracleAlphaMeshAdd>? meshAdds = null;
|
||||
List<WalkOracleAlphaFlush>? flushes = null;
|
||||
List<WalkOraclePortalPolyDraw>? portalPolyDraws = null;
|
||||
List<WalkOracleDrawCellsSample>? drawCellsSamples = null;
|
||||
int currentNumber = 0;
|
||||
|
||||
foreach (string line in lines)
|
||||
{
|
||||
Match frameMatch = FramePattern.Match(line);
|
||||
if (frameMatch.Success)
|
||||
{
|
||||
if (meshAdds is not null)
|
||||
{
|
||||
frames.Add(new WalkOracleAlphaDepthFrame(
|
||||
currentNumber, meshAdds, flushes!, portalPolyDraws!, drawCellsSamples!));
|
||||
}
|
||||
currentNumber = int.Parse(
|
||||
frameMatch.Groups[1].Value, CultureInfo.InvariantCulture);
|
||||
meshAdds = new List<WalkOracleAlphaMeshAdd>();
|
||||
flushes = new List<WalkOracleAlphaFlush>();
|
||||
portalPolyDraws = new List<WalkOraclePortalPolyDraw>();
|
||||
drawCellsSamples = new List<WalkOracleDrawCellsSample>();
|
||||
continue;
|
||||
}
|
||||
if (meshAdds is null)
|
||||
continue;
|
||||
|
||||
Match am = MeshAddPattern.Match(line);
|
||||
if (am.Success)
|
||||
{
|
||||
meshAdds.Add(new WalkOracleAlphaMeshAdd(
|
||||
ParseHex(am.Groups[1].Value),
|
||||
int.Parse(am.Groups[2].Value, CultureInfo.InvariantCulture),
|
||||
ParseHex(am.Groups[3].Value),
|
||||
am.Groups[4].Value != "0",
|
||||
am.Groups[5].Value != "0",
|
||||
int.Parse(am.Groups[6].Value, CultureInfo.InvariantCulture)));
|
||||
continue;
|
||||
}
|
||||
Match fl = FlushPattern.Match(line);
|
||||
if (fl.Success)
|
||||
{
|
||||
flushes!.Add(new WalkOracleAlphaFlush(
|
||||
ParseHex(fl.Groups[1].Value), ParseHex(fl.Groups[2].Value)));
|
||||
continue;
|
||||
}
|
||||
Match pm = PortalPolyPattern.Match(line);
|
||||
if (pm.Success)
|
||||
{
|
||||
portalPolyDraws!.Add(new WalkOraclePortalPolyDraw(
|
||||
ParseHex(pm.Groups[1].Value),
|
||||
int.Parse(pm.Groups[2].Value, CultureInfo.InvariantCulture),
|
||||
int.Parse(pm.Groups[3].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture)));
|
||||
continue;
|
||||
}
|
||||
Match pc = DrawCellsSamplePattern.Match(line);
|
||||
if (pc.Success)
|
||||
{
|
||||
drawCellsSamples!.Add(new WalkOracleDrawCellsSample(
|
||||
int.Parse(pc.Groups[1].Value, CultureInfo.InvariantCulture),
|
||||
int.Parse(pc.Groups[2].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture),
|
||||
pc.Groups[3].Value != "0"));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return frames;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<WalkOracleAlphaDepthFrame> Load(string root, string fixtureName)
|
||||
=> Parse(File.ReadLines(Path.Combine(
|
||||
WalkOracleTraceRepoRoot.Find(),
|
||||
Path.Combine(root.Split('/')),
|
||||
fixtureName + ".alphadepth.log")));
|
||||
|
||||
private static uint ParseHex(string hex)
|
||||
=> uint.Parse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
|
||||
|
||||
private static readonly Regex FramePattern = new(@"^F (\d+)\s*$", RegexOptions.Compiled);
|
||||
|
||||
// AM mesh=<hex8> surf=<n> csurf=<hex8> new=<0|1> clip=<0|1> listSel=<n>
|
||||
private static readonly Regex MeshAddPattern = new(
|
||||
@"^AM mesh=([0-9a-f]{8}) surf=(\d+) csurf=([0-9a-f]{8}) new=(\d) clip=(\d) listSel=(\d+)\s*$",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
// FL thresh=<hex8> ret=<hex8>
|
||||
private static readonly Regex FlushPattern = new(
|
||||
@"^FL thresh=([0-9a-f]{8}) ret=([0-9a-f]{8})\s*$", RegexOptions.Compiled);
|
||||
|
||||
// PM poly=<hex8> mode=<n> counterBefore=<hex4>
|
||||
private static readonly Regex PortalPolyPattern = new(
|
||||
@"^PM poly=([0-9a-f]{8}) mode=(\d) counterBefore=([0-9a-f]{4})\s*$",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
// PC ov=<n> counter=<hex4> fc=<0|1>
|
||||
private static readonly Regex DrawCellsSamplePattern = new(
|
||||
@"^PC ov=(\d+) counter=([0-9a-f]{4}) fc=(\d)\s*$", RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
/// <summary>One <c>D3DPolyRender::AddMeshToAlphaList</c> @0x0059C230 entry
|
||||
/// (AM line). <paramref name="ListSelector"/> 0 selects the ALPHA list,
|
||||
/// nonzero the CLIP list.</summary>
|
||||
public sealed record WalkOracleAlphaMeshAdd(
|
||||
uint Mesh, int Surface, uint ClipSurface, bool New, bool Clip, int ListSelector);
|
||||
|
||||
/// <summary>One <c>D3DPolyRender::FlushAlphaList</c> @0x0059D2E0 entry (FL
|
||||
/// line). <paramref name="ThresholdBits"/> is the raw IEEE-754 bits of the
|
||||
/// threshold float argument.</summary>
|
||||
public sealed record WalkOracleAlphaFlush(uint ThresholdBits, uint ReturnAddress);
|
||||
|
||||
/// <summary>One <c>D3DPolyRender::DrawPortalPolyInternal</c> @0x0059BC90
|
||||
/// entry (PM line). <paramref name="Mode"/> 0 = true-depth/exit-seal,
|
||||
/// nonzero = far-Z/building punch.</summary>
|
||||
public sealed record WalkOraclePortalPolyDraw(uint Poly, int Mode, int CounterBefore);
|
||||
|
||||
/// <summary>One <c>PView::DrawCells</c> @0x005A4840 entry, sampled for the
|
||||
/// persistent <c>portalsDrawnCount</c> depth-lifecycle state machine (PC
|
||||
/// line) — same breakpoint address as the walk log's DC line, but this is a
|
||||
/// SEPARATE capture that does not also record the cell roster.</summary>
|
||||
public sealed record WalkOracleDrawCellsSample(int OutsideViewCount, int Counter, bool ForceClear);
|
||||
|
||||
public sealed record WalkOracleAlphaDepthFrame(
|
||||
int Number,
|
||||
IReadOnlyList<WalkOracleAlphaMeshAdd> MeshAdds,
|
||||
IReadOnlyList<WalkOracleAlphaFlush> Flushes,
|
||||
IReadOnlyList<WalkOraclePortalPolyDraw> PortalPolyDraws,
|
||||
IReadOnlyList<WalkOracleDrawCellsSample> DrawCellsSamples);
|
||||
|
||||
/// <summary>Shared repo-root finder — the SAME walk-up-to-<c>AcDream.slnx</c>
|
||||
/// logic <see cref="WalkOracleTrace"/> already has privately; factored out
|
||||
/// so the parts/alpha-depth parsers don't duplicate it a second and third
|
||||
/// time.</summary>
|
||||
internal static class WalkOracleTraceRepoRoot
|
||||
{
|
||||
internal static string Find()
|
||||
{
|
||||
DirectoryInfo? dir = new(AppContext.BaseDirectory);
|
||||
while (dir is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(dir.FullName, "AcDream.slnx")))
|
||||
return dir.FullName;
|
||||
dir = dir.Parent;
|
||||
}
|
||||
throw new InvalidOperationException(
|
||||
"AcDream.slnx not found above the test base directory; walk-oracle fixtures unavailable.");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
namespace AcDream.App.Tests.Rendering.Walk;
|
||||
|
||||
/// <summary>
|
||||
/// S3 chunk 1 (§11.2 B2/B3): proves the parts/alpha-depth readers actually
|
||||
/// parse the five committed OH captures — "imported and parsed", not just
|
||||
/// compiled. No content is pinned (§11.2 B3: "pinned only where a later
|
||||
/// chunk/S4 names a consumer — no speculative assertions"); this is the
|
||||
/// same non-empty/complete-frame shape
|
||||
/// <c>WalkOracleTraceTests.Fixture_parses_with_complete_frames</c> already
|
||||
/// applies to the walk logs.
|
||||
/// </summary>
|
||||
public sealed class WalkOraclePartsTraceTests
|
||||
{
|
||||
private const string OhRoot = "docs/research/2026-09-01-overhaul/oh-capture";
|
||||
|
||||
public static readonly TheoryData<string> AllPoses = new()
|
||||
{
|
||||
"holtburg-doorway-still",
|
||||
"terrace-edge",
|
||||
"cathedral-arrival",
|
||||
"foundry-deep",
|
||||
"cathedral-leak",
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(AllPoses))]
|
||||
public void Parts_log_parses_with_nonempty_draws(string pose)
|
||||
{
|
||||
IReadOnlyList<WalkOraclePartsFrame> frames = WalkOraclePartsTrace.Load(OhRoot, pose);
|
||||
|
||||
Assert.NotEmpty(frames);
|
||||
Assert.Equal(1, frames[0].Number);
|
||||
Assert.Equal(frames.Count, frames[^1].Number);
|
||||
Assert.Contains(frames, f => f.PartDraws.Count > 0);
|
||||
Assert.Contains(frames, f => f.MeshDraws.Count > 0);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(AllPoses))]
|
||||
public void Alpha_depth_log_parses_with_nonempty_samples(string pose)
|
||||
{
|
||||
IReadOnlyList<WalkOracleAlphaDepthFrame> frames =
|
||||
WalkOracleAlphaDepthTrace.Load(OhRoot, pose);
|
||||
|
||||
Assert.NotEmpty(frames);
|
||||
Assert.Equal(1, frames[0].Number);
|
||||
Assert.Equal(frames.Count, frames[^1].Number);
|
||||
Assert.Contains(frames, f => f.Flushes.Count > 0);
|
||||
Assert.Contains(frames, f => f.DrawCellsSamples.Count > 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -110,6 +110,22 @@ public static class WalkOracleTrace
|
|||
current.Add(WalkOracleEvent.SortCell(ParseId(sortCellMatch.Groups[1].Value)));
|
||||
continue;
|
||||
}
|
||||
// S3 chunk 1 (§11.2 B2): EC/OC — RenderDeviceD3D::DrawEnvCell
|
||||
// 0x0059f170 / DrawObjCellForDummies 0x005a0760, added to the OH
|
||||
// capture templates 2026-09-02
|
||||
// (tools/walk-oracle/oh/oh-capture-walk.cdb.template).
|
||||
Match envCellMatch = EnvCellShellPattern.Match(line);
|
||||
if (envCellMatch.Success)
|
||||
{
|
||||
current.Add(WalkOracleEvent.EnvCellShell(ParseId(envCellMatch.Groups[1].Value)));
|
||||
continue;
|
||||
}
|
||||
Match objCellMatch = ObjectCellTurnPattern.Match(line);
|
||||
if (objCellMatch.Success)
|
||||
{
|
||||
current.Add(WalkOracleEvent.ObjectCellTurn(ParseId(objCellMatch.Groups[1].Value)));
|
||||
continue;
|
||||
}
|
||||
// Anything else is cdb chrome (banner, prompts, symbol notes) — ignored.
|
||||
}
|
||||
|
||||
|
|
@ -160,6 +176,8 @@ public static class WalkOracleTrace
|
|||
@"^DC pv=[0-9a-f]{8} ov=(\d+) n=(\d+):((?: [0-9a-f]{8})*)\s*$", RegexOptions.Compiled);
|
||||
private static readonly Regex LandCellPattern = new(@"^LC ([0-9a-f]{8})\s*$", RegexOptions.Compiled);
|
||||
private static readonly Regex SortCellPattern = new(@"^SC ([0-9a-f]{8})\s*$", RegexOptions.Compiled);
|
||||
private static readonly Regex EnvCellShellPattern = new(@"^EC ([0-9a-f]{8})\s*$", RegexOptions.Compiled);
|
||||
private static readonly Regex ObjectCellTurnPattern = new(@"^OC ([0-9a-f]{8})\s*$", RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
/// <summary>The camera pose dumped at the frame marker (raw dwords from
|
||||
|
|
@ -204,6 +222,16 @@ public enum WalkOracleEventKind
|
|||
/// <summary>S3 chunk 3: <c>SC <cellid></c> — <c>RenderDeviceD3D::
|
||||
/// DrawSortCell</c> @0x0059f140 entry.</summary>
|
||||
SortCell,
|
||||
|
||||
/// <summary>S3 chunk 1 (§11.2 B2): <c>EC <cellid></c> —
|
||||
/// <c>RenderDeviceD3D::DrawEnvCell</c> @0x0059f170 entry (one cell
|
||||
/// SHELL draw).</summary>
|
||||
EnvCellShell,
|
||||
|
||||
/// <summary>S3 chunk 1 (§11.2 B2): <c>OC <cellid></c> —
|
||||
/// <c>RenderDeviceD3D::DrawObjCellForDummies</c> @0x005a0760 entry (one
|
||||
/// cell OBJECT-LIST turn).</summary>
|
||||
ObjectCellTurn,
|
||||
}
|
||||
|
||||
public sealed record WalkOracleEvent(
|
||||
|
|
@ -231,4 +259,10 @@ public sealed record WalkOracleEvent(
|
|||
|
||||
public static WalkOracleEvent SortCell(uint cellId)
|
||||
=> new(WalkOracleEventKind.SortCell, cellId, 0, 0, Array.Empty<uint>());
|
||||
|
||||
public static WalkOracleEvent EnvCellShell(uint cellId)
|
||||
=> new(WalkOracleEventKind.EnvCellShell, cellId, 0, 0, Array.Empty<uint>());
|
||||
|
||||
public static WalkOracleEvent ObjectCellTurn(uint cellId)
|
||||
=> new(WalkOracleEventKind.ObjectCellTurn, cellId, 0, 0, Array.Empty<uint>());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,25 +10,43 @@ public sealed class WalkOracleTraceTests
|
|||
{
|
||||
private const uint FarBuilding = 0xF518002Eu;
|
||||
|
||||
public static readonly TheoryData<string> AllFixtures = new()
|
||||
private const string FwRoot = "docs/research/2026-08-30-fw-walk-oracle";
|
||||
|
||||
/// <summary>S3 chunk 1 (§11.2 B3): the OH capture directory. Its walk
|
||||
/// logs are named <c><pose>.walk.log</c> — the trailing
|
||||
/// <c>.walk</c> below is the fixture-name half of that filename, not a
|
||||
/// subdirectory (matching <see cref="WalkLandCellOrderTests"/>'s own
|
||||
/// convention).</summary>
|
||||
private const string OhRoot = "docs/research/2026-09-01-overhaul/oh-capture";
|
||||
|
||||
/// <summary>(root, fixture name) pairs. Every FW0 still/posed fixture
|
||||
/// keeps its old bare name under <see cref="FwRoot"/>; the five OH
|
||||
/// walk captures (§11.2 B3) join under <see cref="OhRoot"/> with the
|
||||
/// <c>.walk</c> filename-half suffix.</summary>
|
||||
public static readonly TheoryData<string, string> AllFixtures = new()
|
||||
{
|
||||
"terrace-center",
|
||||
"terrace-edge",
|
||||
"cathedral-arrival",
|
||||
"holtburg-doorway-still",
|
||||
"holtburg-walkout",
|
||||
"holtburg-street-porchcam",
|
||||
"holtburg-street-outdoor",
|
||||
"holtburg-walkabout",
|
||||
"foundry-entry",
|
||||
"foundry-deep",
|
||||
{ FwRoot, "terrace-center" },
|
||||
{ FwRoot, "terrace-edge" },
|
||||
{ FwRoot, "cathedral-arrival" },
|
||||
{ FwRoot, "holtburg-doorway-still" },
|
||||
{ FwRoot, "holtburg-walkout" },
|
||||
{ FwRoot, "holtburg-street-porchcam" },
|
||||
{ FwRoot, "holtburg-street-outdoor" },
|
||||
{ FwRoot, "holtburg-walkabout" },
|
||||
{ FwRoot, "foundry-entry" },
|
||||
{ FwRoot, "foundry-deep" },
|
||||
{ OhRoot, "holtburg-doorway-still.walk" },
|
||||
{ OhRoot, "terrace-edge.walk" },
|
||||
{ OhRoot, "cathedral-arrival.walk" },
|
||||
{ OhRoot, "foundry-deep.walk" },
|
||||
{ OhRoot, "cathedral-leak.walk" },
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(AllFixtures))]
|
||||
public void Fixture_parses_with_complete_frames(string name)
|
||||
public void Fixture_parses_with_complete_frames(string root, string name)
|
||||
{
|
||||
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Load(name);
|
||||
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Load(root, name);
|
||||
|
||||
Assert.NotEmpty(frames);
|
||||
// Frame numbers are contiguous from 1; the truncated final frame is dropped.
|
||||
|
|
|
|||
|
|
@ -100,13 +100,57 @@ public sealed class WalkTraceConformanceTests
|
|||
$"walk diverged from retail\nEXPECTED: {expected}\nACTUAL: {actual}");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("posed/terrace-center")]
|
||||
[InlineData("posed/terrace-edge")]
|
||||
[InlineData("posed/cathedral-arrival")]
|
||||
public void Still_fixture_first_frame_reproduces_exactly(string fixture)
|
||||
[Fact]
|
||||
public void Oh_doorway_still_first_frame_diff()
|
||||
{
|
||||
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Load(fixture);
|
||||
// S3 chunk 1 (§11.2 B3): the OH kit pose's own doorway-still capture
|
||||
// — DI f4180108... no, a9b4013f, DC(ov=2, n=3) per §6b. Mirrors
|
||||
// Doorway_still_first_frame_diff's own structure (this pose needs
|
||||
// the interior camera cell, unlike the plain outdoor/theory rows).
|
||||
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Load(
|
||||
"docs/research/2026-09-01-overhaul/oh-capture", "holtburg-doorway-still.walk");
|
||||
Assert.NotEmpty(frames);
|
||||
using DatCollection dats = OpenDats();
|
||||
WalkOracleFrame frame = frames[1];
|
||||
Assert.NotNull(frame.Pose);
|
||||
WalkLandscapeDatBuilder.BuiltWorld world =
|
||||
WalkLandscapeDatBuilder.Build(dats, frame.Pose!.CellId, frame.Pose.Origin);
|
||||
var ctx = new WalkTraceReplayContext(frame.Pose, world.Cells)
|
||||
{
|
||||
Buildings = world.Buildings,
|
||||
};
|
||||
WalkCell camera = Assert.Contains(frame.Pose.CellId, world.Cells);
|
||||
var walk = new RetailFrameWalk();
|
||||
var recorder = new Recorder();
|
||||
|
||||
walk.WalkFrame(frame.Pose.CellId, camera, world.Landscape, ctx, recorder);
|
||||
|
||||
string expected = WalkTraceReplayContext.Signature(frame);
|
||||
string actual = WalkTraceReplayContext.Signature(recorder.Events);
|
||||
Assert.True(
|
||||
expected == actual,
|
||||
$"walk diverged from retail\nEXPECTED: {expected}\nACTUAL: {actual}");
|
||||
}
|
||||
|
||||
private const string FwOracleRoot = "docs/research/2026-08-30-fw-walk-oracle";
|
||||
|
||||
/// <summary>S3 chunk 1 (§11.2 B3): the OH capture directory — its OWN
|
||||
/// pose-stamped kit-pose captures, a DIFFERENT root than the FW0 still
|
||||
/// fixtures above.</summary>
|
||||
private const string OhCaptureRoot = "docs/research/2026-09-01-overhaul/oh-capture";
|
||||
|
||||
[Theory]
|
||||
[InlineData(FwOracleRoot, "posed/terrace-center")]
|
||||
[InlineData(FwOracleRoot, "posed/terrace-edge")]
|
||||
[InlineData(FwOracleRoot, "posed/cathedral-arrival")]
|
||||
// S3 chunk 1 (§11.2 B3): the OH kit poses, as NEW rows — the OH
|
||||
// cathedral-arrival root is f4180108, NOT FW0's f4180106 (a new pose,
|
||||
// not a replacement of the FW0 row above).
|
||||
[InlineData(OhCaptureRoot, "terrace-edge.walk")]
|
||||
[InlineData(OhCaptureRoot, "cathedral-arrival.walk")]
|
||||
public void Still_fixture_first_frame_reproduces_exactly(string root, string fixture)
|
||||
{
|
||||
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Load(root, fixture);
|
||||
Assert.NotEmpty(frames);
|
||||
using DatCollection dats = OpenDats();
|
||||
WalkOracleFrame frame = frames[1];
|
||||
|
|
@ -240,4 +284,48 @@ public sealed class WalkTraceConformanceTests
|
|||
WalkTraceReplayContext.Signature(recorder.Events));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Oh_foundry_deep_reproduces_every_complete_frame_exactly()
|
||||
{
|
||||
// S3 chunk 1 (§11.2 B3): the OH kit pose's own foundry-deep capture
|
||||
// — DI a9b40176, DC(ov=1, n=2), 12 town buildings drawn through the
|
||||
// surviving exit chain (§6b). UNLIKE the FW0 sibling above, this
|
||||
// capture's own retail transcript shows real BLD content at that
|
||||
// depth, so the stub 1x1 landscape (which has no blocks/buildings to
|
||||
// walk at all) undershoots it — first divergence, run without the
|
||||
// fix below: "DI:a9b40176|DC:ov=1:a9b40176,a9b40177|LS" (nothing
|
||||
// after LS) vs retail's "…|LS|BLD:a9b40031|BLD:a9b…" (12 real
|
||||
// buildings). The full landscape/building assembler
|
||||
// (WalkLandscapeDatBuilder.Build — the SAME one the shared theory
|
||||
// and Oh_doorway_still_first_frame_diff use) reproduces them; the
|
||||
// camera is stationary across this still pose, so one build serves
|
||||
// every frame in the loop, matching the FW0 sibling's "build once"
|
||||
// shape.
|
||||
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Load(
|
||||
"docs/research/2026-09-01-overhaul/oh-capture", "foundry-deep.walk");
|
||||
Assert.NotEmpty(frames);
|
||||
using DatCollection dats = OpenDats();
|
||||
Assert.NotNull(frames[0].Pose);
|
||||
WalkLandscapeDatBuilder.BuiltWorld world = WalkLandscapeDatBuilder.Build(
|
||||
dats, frames[0].Pose!.CellId, frames[0].Pose!.Origin);
|
||||
|
||||
foreach (WalkOracleFrame frame in frames)
|
||||
{
|
||||
Assert.NotNull(frame.Pose);
|
||||
WalkCell camera = Assert.Contains(frame.Pose!.CellId, world.Cells);
|
||||
var ctx = new WalkTraceReplayContext(frame.Pose, world.Cells)
|
||||
{
|
||||
Buildings = world.Buildings,
|
||||
};
|
||||
var walk = new RetailFrameWalk();
|
||||
var recorder = new Recorder();
|
||||
|
||||
walk.WalkFrame(frame.Pose.CellId, camera, world.Landscape, ctx, recorder);
|
||||
|
||||
Assert.Equal(
|
||||
WalkTraceReplayContext.Signature(frame),
|
||||
WalkTraceReplayContext.Signature(recorder.Events));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,8 +146,26 @@ public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalk
|
|||
_ => "?",
|
||||
}));
|
||||
|
||||
/// <summary>
|
||||
/// S3 chunk 1 (§11.2 B3): the OH captures interleave <c>LC</c>/<c>SC</c>/
|
||||
/// <c>EC</c>/<c>OC</c> lines the pre-chunk-3 FW0 fixtures never had.
|
||||
/// <see cref="RetailFrameWalk"/>'s own <c>WalkEvent</c> vocabulary has
|
||||
/// exactly four kinds (Landscape/Building/DrawInside/DrawCells) — LC/SC/
|
||||
/// EC/OC are separate <see cref="IWalkEventSink"/> hooks
|
||||
/// <see cref="Recorder"/> never overrides, so the replay side of a
|
||||
/// signature diff is silent on them by construction. Filtering them out
|
||||
/// here (rather than mapping to a "?" placeholder) keeps this
|
||||
/// comparison at the SAME DI/DC/BLD/LS level on both sides — S3's own
|
||||
/// scope note ("no speculative pins" for LC/SC/EC/OC content) means this
|
||||
/// method must not even attempt to compare them, not merely fail to.
|
||||
/// </summary>
|
||||
public static string Signature(WalkOracleFrame frame)
|
||||
=> string.Join("|", frame.Events.Select(e => e.Kind switch
|
||||
=> string.Join("|", frame.Events
|
||||
.Where(e => e.Kind is WalkOracleEventKind.Landscape
|
||||
or WalkOracleEventKind.Building
|
||||
or WalkOracleEventKind.DrawInside
|
||||
or WalkOracleEventKind.DrawCells)
|
||||
.Select(e => e.Kind switch
|
||||
{
|
||||
WalkOracleEventKind.Landscape => "LS",
|
||||
WalkOracleEventKind.Building => $"BLD:{e.CellId!.Value:x8}",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
namespace AcDream.App.Tests.Rendering.Walk;
|
||||
|
||||
/// <summary>
|
||||
/// S3 chunk 1 (§11.2 B4): the offline transcript signature diff. Given an
|
||||
/// acdream transcript (printed by the production
|
||||
/// <c>ACDREAM_DUMP_WALK_TRANSCRIPT=1</c> emitter — <c>WalkTranscriptDump</c>)
|
||||
/// and a retail capture (or any two <see cref="WalkOracleTrace"/>-parsed
|
||||
/// transcripts), reports the FIRST divergent event and its position, frame
|
||||
/// by frame, at the DI/DC/BLD/LS/LC/SC/EC/OC level. A test-side comparison
|
||||
/// utility only — no runner, no dashboard, no new tool under
|
||||
/// <c>tools/</c> ([[feedback-evidence-infrastructure-sink]]); the S3 review
|
||||
/// and G3 consume its report by hand.
|
||||
/// </summary>
|
||||
public static class WalkTranscriptSignatureDiff
|
||||
{
|
||||
/// <summary>The first point two transcripts disagree, or
|
||||
/// <see langword="null"/> when every frame both sides share matches
|
||||
/// exactly. <paramref name="FrameNumber"/> is the retail-side frame's
|
||||
/// own number (matching the "F n" line); <paramref name="Position"/>
|
||||
/// is the zero-based index into that frame's DI/DC/BLD/LS/LC/SC/EC/OC
|
||||
/// event signature. <c>"<end of frame>"</c>/<c>"<missing
|
||||
/// frame>"</c> mark a length mismatch rather than a content
|
||||
/// mismatch.</summary>
|
||||
public readonly record struct Divergence(
|
||||
int FrameNumber, int Position, string Expected, string Actual);
|
||||
|
||||
/// <summary>Diffs two raw transcripts (e.g. one file read as lines each)
|
||||
/// — the file-to-file form B4 names directly.</summary>
|
||||
public static Divergence? FirstDivergence(
|
||||
IEnumerable<string> expectedLines, IEnumerable<string> actualLines)
|
||||
=> FirstDivergence(
|
||||
WalkOracleTrace.Parse(expectedLines), WalkOracleTrace.Parse(actualLines));
|
||||
|
||||
/// <summary>Diffs two already-parsed frame lists — the form the
|
||||
/// synthetic self-test below uses directly, without a round trip
|
||||
/// through a temp file.</summary>
|
||||
public static Divergence? FirstDivergence(
|
||||
IReadOnlyList<WalkOracleFrame> expected, IReadOnlyList<WalkOracleFrame> actual)
|
||||
{
|
||||
int frameCount = Math.Min(expected.Count, actual.Count);
|
||||
for (int f = 0; f < frameCount; f++)
|
||||
{
|
||||
IReadOnlyList<string> e = Signature(expected[f]);
|
||||
IReadOnlyList<string> a = Signature(actual[f]);
|
||||
int shared = Math.Min(e.Count, a.Count);
|
||||
for (int i = 0; i < shared; i++)
|
||||
{
|
||||
if (!string.Equals(e[i], a[i], StringComparison.Ordinal))
|
||||
return new Divergence(expected[f].Number, i, e[i], a[i]);
|
||||
}
|
||||
if (e.Count != a.Count)
|
||||
{
|
||||
return new Divergence(
|
||||
expected[f].Number,
|
||||
shared,
|
||||
shared < e.Count ? e[shared] : "<end of frame>",
|
||||
shared < a.Count ? a[shared] : "<end of frame>");
|
||||
}
|
||||
}
|
||||
if (expected.Count != actual.Count)
|
||||
{
|
||||
int frameNumber = frameCount < expected.Count
|
||||
? expected[frameCount].Number
|
||||
: actual[frameCount].Number;
|
||||
return new Divergence(
|
||||
frameNumber,
|
||||
0,
|
||||
frameCount < expected.Count ? "<frame present>" : "<missing frame>",
|
||||
frameCount < actual.Count ? "<frame present>" : "<missing frame>");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>One frame's DI/DC/BLD/LS/LC/SC/EC/OC turns, in transcript
|
||||
/// order — the full eight-kind vocabulary B4 names (unlike
|
||||
/// <see cref="WalkTraceReplayContext.Signature(WalkOracleFrame)"/>,
|
||||
/// which deliberately stays at the pre-S3-chunk-3 four-kind level for
|
||||
/// the B3 still-fixture rows).</summary>
|
||||
private static IReadOnlyList<string> Signature(WalkOracleFrame frame)
|
||||
=> frame.Events.Select(e => e.Kind switch
|
||||
{
|
||||
WalkOracleEventKind.Landscape => "LS",
|
||||
WalkOracleEventKind.Building => $"BLD:{e.CellId!.Value:x8}",
|
||||
WalkOracleEventKind.DrawInside => $"DI:{e.CellId!.Value:x8}",
|
||||
WalkOracleEventKind.DrawCells =>
|
||||
$"DC:ov={e.OutsideViewCount}:{string.Join(',', e.Cells.Select(c => c.ToString("x8")))}",
|
||||
WalkOracleEventKind.LandCell => $"LC:{e.CellId!.Value:x8}",
|
||||
WalkOracleEventKind.SortCell => $"SC:{e.CellId!.Value:x8}",
|
||||
WalkOracleEventKind.EnvCellShell => $"EC:{e.CellId!.Value:x8}",
|
||||
WalkOracleEventKind.ObjectCellTurn => $"OC:{e.CellId!.Value:x8}",
|
||||
_ => "?",
|
||||
}).ToList();
|
||||
}
|
||||
|
|
@ -155,6 +155,7 @@ public sealed class RuntimeOptionsTests
|
|||
Assert.False(opts.DevTools);
|
||||
Assert.False(opts.UncappedRendering);
|
||||
Assert.False(opts.DumpMoveTruth);
|
||||
Assert.False(opts.DumpWalkTranscript);
|
||||
Assert.False(opts.NoAudio);
|
||||
Assert.Equal(-1, opts.HidePartIndex);
|
||||
// Default-on: RetailCloseDegrades is true unless explicitly disabled.
|
||||
|
|
@ -521,6 +522,7 @@ public sealed class RuntimeOptionsTests
|
|||
["ACDREAM_UNCAPPED_RENDER"] = "1",
|
||||
["ACDREAM_DUMP_MOVE_TRUTH"] = "1",
|
||||
["ACDREAM_DUMP_SKY"] = "1",
|
||||
["ACDREAM_DUMP_WALK_TRANSCRIPT"] = "1",
|
||||
["ACDREAM_NO_AUDIO"] = "1",
|
||||
["ACDREAM_DUMP_SCENERY_Z"] = "1",
|
||||
}));
|
||||
|
|
@ -528,6 +530,7 @@ public sealed class RuntimeOptionsTests
|
|||
Assert.True(allOn.UncappedRendering);
|
||||
Assert.True(allOn.DumpMoveTruth);
|
||||
Assert.True(allOn.DumpSky);
|
||||
Assert.True(allOn.DumpWalkTranscript);
|
||||
Assert.True(allOn.NoAudio);
|
||||
Assert.True(allOn.DumpSceneryZ);
|
||||
|
||||
|
|
@ -540,12 +543,14 @@ public sealed class RuntimeOptionsTests
|
|||
["ACDREAM_UNCAPPED_RENDER"] = "true",
|
||||
["ACDREAM_NO_AUDIO"] = "2",
|
||||
["ACDREAM_DUMP_SCENERY_Z"] = " 1",
|
||||
["ACDREAM_DUMP_WALK_TRANSCRIPT"] = "0",
|
||||
}));
|
||||
Assert.False(anyOther.DevTools);
|
||||
Assert.False(anyOther.UncappedRendering);
|
||||
Assert.False(anyOther.DumpMoveTruth);
|
||||
Assert.False(anyOther.NoAudio);
|
||||
Assert.False(anyOther.DumpSceneryZ);
|
||||
Assert.False(anyOther.DumpWalkTranscript);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue