#119-residual: tower-ascent harness pins the roof-lip flood gap; barrel claim RETRACTED (user axiom: not in retail)
User verdict on the post-#120 build: "Barrel is gone and more stairs exist" - the #120 fix partially cured the tower, and the earlier "legit dat barrels on the landings" claim is RETRACTED (USER AXIOM: the barrel is NOT in the tower in retail; what the user saw was itself a render artifact of the corrupted floods, and what the 0x020005D8 cell statics actually render as is unverified - do not assume barrel). Remaining tower bugs, both PINNED by TowerAscentReplayTests (the #118 exit-walk pattern, vertical - a helix ascent with the gaze locked ON the staircase, so a cull has no gaze excuse): - steps 195-201 (eye z 126.9-127.3, the roof-lip band between the main cell's ceiling at 126.8 and the roof aperture plane at ~127.2) resolve OUTDOOR and the per-building exterior flood admits NOTHING (flood=1 = the outdoor node alone): the eye is above every side aperture's useful view and ON/INSIDE the roof aperture's plane, so BuildFromExterior's seed side-test / in-plane reject refuses every exit portal. The tower interior never floods -> the staircase (a 0x0107 static) cone-culls while staying walkable (user symptom 1), and the roof-lip cell geometry flaps as the live eye bobs across the band's edges (user symptom 2). One mechanism, both symptoms. - The pin is committed as a SKIPPED red test (TowerAscent_StaircaseStaysConeVisible_EveryStep; the skip reason carries the defect) so the suite stays green - un-skip with the fix. - TowerAscent_RootDoesNotPingPong + the per-step diagnostic stay active. Fix direction (oracle-first, next): determine which side diverges from retail - (a) viewer-cell resolution (retail curr_cell may keep the eye INTERIOR through the band: keep-curr above open-top cells / cell BSP classifying the parapet bowl as inside 0x010A, where our resolution demotes to outdoor), or (b) exterior seed admission (retail ConstructView(CBldPortal) Sidedness with an in-plane eye). Grep the named decomp for both before touching either layer. Suites: App 238 + 1 skip (236+3 new, 1 pinned), Core 1419+2skip, UI 420, Net 294. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0c55b473dd
commit
899145e1d7
3 changed files with 303 additions and 18 deletions
|
|
@ -4077,23 +4077,50 @@ area:**
|
||||||
tracked under #112; note the #120 ping-pong fired at exactly A9B3
|
tracked under #112; note the #120 ping-pong fired at exactly A9B3
|
||||||
0103↔010F, so re-check after the #120 fix (`dede7e4`).
|
0103↔010F, so re-check after the #120 fix (`dede7e4`).
|
||||||
|
|
||||||
**DECODED + LIKELY FIXED BY #120 (2026-06-11 evening):** the user's
|
**DECODED (2026-06-11 evening):** the user's logout position pinned the
|
||||||
logout position pinned the tower (cell 0xAAB30107, AAB3 building[1]
|
tower (cell 0xAAB30107, AAB3 building[1] model 0x01001117). Dat truth
|
||||||
model 0x01001117). Dat truth (`Issue119TowerDumpTests`): the stairs are
|
(`Issue119TowerDumpTests`): the stairs are ONE static — Setup
|
||||||
ONE static — Setup 0x020003F2, a 43-part spiral staircase at the tower
|
0x020003F2, a 43-part spiral staircase at the tower center (placement
|
||||||
center (placement frames perfect, all parts drawable); the "extraneous
|
frames perfect, all parts drawable). Pipeline exonerated layer by layer
|
||||||
barrel" = the four 0x020005D8 wall barrels on the landings — legit dat
|
(extraction, hydration ParentCellId=envCellId, per-MeshRef registration,
|
||||||
statics orphaned by the missing staircase. Pipeline exonerated layer by
|
dispatcher compose); clean WB_DIAG counters at the tower spawn:
|
||||||
layer (extraction, hydration ParentCellId=envCellId, per-MeshRef
|
meshMissing=0, entSeen==entDrawn.
|
||||||
registration, dispatcher compose); clean WB_DIAG counters at the tower
|
|
||||||
spawn: meshMissing=0, entSeen==entDrawn. **A screenshot of the running
|
**⚠️ USER AXIOM (2026-06-11 late): the barrel is NOT in the tower in
|
||||||
post-fix build shows the staircase RENDERING.** Mechanism (most
|
retail.** The earlier "legit dat barrels on the landings" claim is
|
||||||
plausible): the tower's three threshold cells portal back to 0x0107 —
|
RETRACTED — what the user saw was itself a render artifact. Post-#120
|
||||||
climbing the stairs walks the eye through those portal planes, exactly
|
verdict: "Barrel is gone and more stairs exist" — both improved
|
||||||
the #120 ping-pong window; the corrupted/aborted floods broke the
|
together, consistent with the "barrel" being mis-drawn staircase
|
||||||
statics' viewcone per frame. The original report predates the #120 fix;
|
geometry under the corrupted floods. (What the four 0x020005D8 cell
|
||||||
re-gate #2 did not re-check the stairs. **Pending: user verdict on the
|
statics actually render as remains UNVERIFIED — do not assume barrel.)
|
||||||
tower stairs + barrels in the current build** — if clean, close #119.
|
|
||||||
|
**REMAINING (user, post-#120 build):**
|
||||||
|
1. Running UP the tower, the TOP stairs disappear visually but stay
|
||||||
|
walkable.
|
||||||
|
2. On top of the tower, the roof and edges FLAP into existence and
|
||||||
|
back.
|
||||||
|
|
||||||
|
**PINNED 2026-06-11 late (`TowerAscentReplayTests`, gaze locked ON the
|
||||||
|
staircase — no gaze excuse):** in the roof-lip band (eye z ≈ 126.9–127.3,
|
||||||
|
between the main cell's ceiling at 126.8 and the roof aperture plane at
|
||||||
|
~127.2) the eye resolves OUTDOOR and the per-building exterior flood
|
||||||
|
admits NOTHING (flood=1 = the outdoor node alone): the eye sits above
|
||||||
|
every side aperture's useful view and ON/INSIDE the roof aperture's
|
||||||
|
plane, so `BuildFromExterior`'s seed side-test / in-plane reject refuses
|
||||||
|
every exit portal. The tower interior never floods → the staircase
|
||||||
|
(a 0x0107 static) culls while walkable; the roof-lip cell geometry flaps
|
||||||
|
as the live eye bobs across the band's edges. The pin is committed as a
|
||||||
|
skipped red test (`TowerAscent_StaircaseStaysConeVisible_EveryStep`,
|
||||||
|
skip reason carries the defect) — un-skip with the fix.
|
||||||
|
|
||||||
|
**Fix needs the retail oracle (next):** which side diverges —
|
||||||
|
(a) the VIEWER-CELL resolution (retail's curr_cell may keep the eye
|
||||||
|
INTERIOR through this band: keep-curr above open-top cells / the cell
|
||||||
|
BSP classifying the parapet bowl as inside 0x010A, where our resolution
|
||||||
|
demotes to outdoor), or (b) the exterior seed admission (retail
|
||||||
|
ConstructView(CBldPortal) Sidedness on an in-plane eye). Grep
|
||||||
|
`ConstructView` CBldPortal + `SmartBox::update_viewer` above open-top
|
||||||
|
cells before touching either layer.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ public class Issue120ReciprocalPingPongTests
|
||||||
private readonly ITestOutputHelper _out;
|
private readonly ITestOutputHelper _out;
|
||||||
public Issue120ReciprocalPingPongTests(ITestOutputHelper output) => _out = output;
|
public Issue120ReciprocalPingPongTests(ITestOutputHelper output) => _out = output;
|
||||||
|
|
||||||
private static Dictionary<uint, LoadedCell> LoadAllInteriorCells(DatCollection dats, uint landblock)
|
internal static Dictionary<uint, LoadedCell> LoadAllInteriorCells(DatCollection dats, uint landblock)
|
||||||
{
|
{
|
||||||
var lbi = dats.Get<DatLandBlockInfo>(landblock | 0xFFFEu);
|
var lbi = dats.Get<DatLandBlockInfo>(landblock | 0xFFFEu);
|
||||||
Assert.NotNull(lbi);
|
Assert.NotNull(lbi);
|
||||||
|
|
|
||||||
258
tests/AcDream.App.Tests/Rendering/TowerAscentReplayTests.cs
Normal file
258
tests/AcDream.App.Tests/Rendering/TowerAscentReplayTests.cs
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.App.Rendering;
|
||||||
|
using DatReaderWriter;
|
||||||
|
using DatReaderWriter.Options;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// #119 residual — the tower-ascent harness (the #118 exit-walk pattern,
|
||||||
|
/// vertical). User report (post-#120 build): running UP the AAB3 tower
|
||||||
|
/// (cell 0xAAB30107, building[1] model 0x01001117) the TOP stairs disappear
|
||||||
|
/// visually but stay walkable, and at the top the roof/edges FLAP in and
|
||||||
|
/// out. The tower's upper cells are thin landing/roof slabs with EXIT
|
||||||
|
/// portals (0x0108 z≈121–123, 0x0109 z≈112–114.5, 0x010A z≈126.8–127.2 —
|
||||||
|
/// the roof lip), so the ascent walks the eye across exit-portal planes —
|
||||||
|
/// the same decision-stack territory as #118/#120.
|
||||||
|
///
|
||||||
|
/// Per step of a helix matching the spiral staircase, this drives the
|
||||||
|
/// production stack headlessly: viewer-cell resolution (smallest containing
|
||||||
|
/// AABB; outdoor when none) → PortalVisibilityBuilder.Build (+ the
|
||||||
|
/// per-building exterior floods on outdoor steps, mirroring
|
||||||
|
/// RetailPViewRenderer.MergeNearbyBuildingFloods) → ClipFrameAssembler →
|
||||||
|
/// ViewconeCuller → the DrawCellObjectLists predicate for the staircase
|
||||||
|
/// static (Setup 0x020003F2, ParentCellId 0x0107). The failing steps pin
|
||||||
|
/// the vanish; root/visibility instability across adjacent steps pins the
|
||||||
|
/// flap.
|
||||||
|
/// </summary>
|
||||||
|
public class TowerAscentReplayTests
|
||||||
|
{
|
||||||
|
private readonly ITestOutputHelper _out;
|
||||||
|
public TowerAscentReplayTests(ITestOutputHelper output) => _out = output;
|
||||||
|
|
||||||
|
private const uint Landblock = 0xAAB30000u;
|
||||||
|
private const uint MainCell = Landblock | 0x0107u;
|
||||||
|
|
||||||
|
// Tower geometry (Issue119TowerDumpTests dat facts): origin (108,60,112);
|
||||||
|
// the staircase Setup spans local x,y ∈ [-4,4], z ∈ [0,15.5]. Production
|
||||||
|
// EntitySphere = AABB center + half-diagonal.
|
||||||
|
private static readonly Vector3 TowerCenter = new(108f, 60f, 112f);
|
||||||
|
private static readonly Vector3 StairSphereCenter = new(108f, 60f, 119.75f);
|
||||||
|
private const float StairSphereRadius = 9.45f;
|
||||||
|
|
||||||
|
private sealed record AscentStep(
|
||||||
|
int Index,
|
||||||
|
Vector3 Eye,
|
||||||
|
uint RootCellId, // 0 = outdoor
|
||||||
|
bool FloodHasMainCell,
|
||||||
|
bool StairsConeVisible,
|
||||||
|
int OutsidePolys,
|
||||||
|
int FloodCells);
|
||||||
|
|
||||||
|
private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 lookAt)
|
||||||
|
{
|
||||||
|
var view = Matrix4x4.CreateLookAt(eye, lookAt, Vector3.UnitZ);
|
||||||
|
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1280f / 720f, 0.1f, 5000f);
|
||||||
|
return view * proj;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint? ResolveInteriorCellByAabb(
|
||||||
|
Dictionary<uint, LoadedCell> cells, Vector3 worldPoint, float margin = 0.05f)
|
||||||
|
{
|
||||||
|
uint? best = null;
|
||||||
|
float bestVolume = float.MaxValue;
|
||||||
|
foreach (var (id, cell) in cells)
|
||||||
|
{
|
||||||
|
var local = Vector3.Transform(worldPoint, cell.InverseWorldTransform);
|
||||||
|
var min = cell.LocalBoundsMin - new Vector3(margin);
|
||||||
|
var max = cell.LocalBoundsMax + new Vector3(margin);
|
||||||
|
if (local.X < min.X || local.Y < min.Y || local.Z < min.Z
|
||||||
|
|| local.X > max.X || local.Y > max.Y || local.Z > max.Z)
|
||||||
|
continue;
|
||||||
|
var ext = cell.LocalBoundsMax - cell.LocalBoundsMin;
|
||||||
|
float volume = MathF.Max(ext.X, 1e-3f) * MathF.Max(ext.Y, 1e-3f) * MathF.Max(ext.Z, 1e-3f);
|
||||||
|
if (volume < bestVolume) { bestVolume = volume; best = id; }
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirror of RetailPViewRenderer.MergeBuildingFrame (union semantics).
|
||||||
|
private static void MergeFrame(PortalVisibilityFrame target, PortalVisibilityFrame src)
|
||||||
|
{
|
||||||
|
foreach (uint cellId in src.OrderedVisibleCells)
|
||||||
|
{
|
||||||
|
if (!src.CellViews.TryGetValue(cellId, out var srcView)) continue;
|
||||||
|
if (target.CellViews.TryGetValue(cellId, out var existing))
|
||||||
|
{
|
||||||
|
foreach (var p in srcView.Polygons) existing.Add(p);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
target.CellViews[cellId] = srcView;
|
||||||
|
target.OrderedVisibleCells.Add(cellId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<AscentStep>? RunAscent()
|
||||||
|
{
|
||||||
|
var datDir = CornerFloodReplayTests.ResolveDatDir();
|
||||||
|
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return null; }
|
||||||
|
|
||||||
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||||
|
var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, Landblock);
|
||||||
|
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
|
||||||
|
Assert.True(cells.ContainsKey(MainCell), "tower main cell not loaded");
|
||||||
|
|
||||||
|
// The tower building's cell list (Issue119TowerDumpTests: building[1]
|
||||||
|
// stabs = 0x0107..0x010A) — the per-building flood candidates on
|
||||||
|
// outdoor-root steps, mirroring GameWindow's gather.
|
||||||
|
var towerCells = new List<LoadedCell>();
|
||||||
|
for (uint low = 0x0107u; low <= 0x010Au; low++)
|
||||||
|
if (cells.TryGetValue(Landblock | low, out var c))
|
||||||
|
towerCells.Add(c);
|
||||||
|
|
||||||
|
// Eye helix matching the spiral: two turns, radius 2.5 m, z from head
|
||||||
|
// height at the base to above the roof lip (cell 0x010A tops at 127.2).
|
||||||
|
const int Steps = 220;
|
||||||
|
const float ZStart = 113.6f, ZEnd = 128.6f;
|
||||||
|
var clipFrame = ClipFrame.NoClip();
|
||||||
|
var steps = new List<AscentStep>(Steps + 1);
|
||||||
|
|
||||||
|
for (int i = 0; i <= Steps; i++)
|
||||||
|
{
|
||||||
|
float t = i / (float)Steps;
|
||||||
|
float theta = t * 4f * MathF.PI;
|
||||||
|
float z = ZStart + t * (ZEnd - ZStart);
|
||||||
|
var eye = new Vector3(
|
||||||
|
TowerCenter.X + 2.5f * MathF.Cos(theta),
|
||||||
|
TowerCenter.Y + 2.5f * MathF.Sin(theta),
|
||||||
|
z);
|
||||||
|
// look AT the staircase (the user's framing while climbing: the
|
||||||
|
// camera tracks the player on the stairs). An unambiguous pin:
|
||||||
|
// if the staircase cone-culls while looked at directly, the
|
||||||
|
// visibility stack is wrong — no gaze excuse.
|
||||||
|
var gaze = StairSphereCenter - eye;
|
||||||
|
if (gaze.LengthSquared() < 1e-4f) gaze = new Vector3(0f, 0f, -1f);
|
||||||
|
var viewProj = ViewProjFor(eye, eye + Vector3.Normalize(gaze) * 3f);
|
||||||
|
|
||||||
|
uint rootCellId = 0u;
|
||||||
|
PortalVisibilityFrame pv;
|
||||||
|
var contained = ResolveInteriorCellByAabb(cells, eye);
|
||||||
|
if (contained.HasValue)
|
||||||
|
{
|
||||||
|
rootCellId = contained.Value;
|
||||||
|
pv = PortalVisibilityBuilder.Build(cells[rootCellId], eye, lookup, viewProj);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// outdoor root: full-screen outside view + the per-building
|
||||||
|
// exterior floods (RetailPViewRenderer.MergeNearbyBuildingFloods)
|
||||||
|
var outdoor = OutdoorCellNode.Build(Landblock | 0x0001u);
|
||||||
|
pv = PortalVisibilityBuilder.Build(outdoor, eye, lookup, viewProj);
|
||||||
|
var bf = PortalVisibilityBuilder.ConstructViewBuilding(
|
||||||
|
towerCells, eye, lookup, viewProj);
|
||||||
|
MergeFrame(pv, bf);
|
||||||
|
}
|
||||||
|
|
||||||
|
var asm = ClipFrameAssembler.Assemble(clipFrame, pv);
|
||||||
|
var cone = ViewconeCuller.Build(asm, viewProj);
|
||||||
|
|
||||||
|
bool floodHasMain = pv.CellViews.ContainsKey(MainCell);
|
||||||
|
// DrawCellObjectLists predicate for a 0x0107 static: sphere vs the
|
||||||
|
// cell's views (RetailPViewRenderer.cs DrawCellObjectLists / T3).
|
||||||
|
bool stairsVisible = cone.SphereVisibleInCell(MainCell, StairSphereCenter, StairSphereRadius);
|
||||||
|
|
||||||
|
steps.Add(new AscentStep(
|
||||||
|
i, eye, rootCellId, floodHasMain, stairsVisible,
|
||||||
|
pv.OutsideView.Polygons.Count, pv.OrderedVisibleCells.Count));
|
||||||
|
}
|
||||||
|
|
||||||
|
return steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DumpSteps(IEnumerable<AscentStep> steps, Func<AscentStep, bool>? filter = null)
|
||||||
|
{
|
||||||
|
foreach (var st in steps)
|
||||||
|
{
|
||||||
|
if (filter is not null && !filter(st)) continue;
|
||||||
|
string root = st.RootCellId == 0 ? "OUTDOOR " : FormattableString.Invariant($"0x{st.RootCellId:X8}");
|
||||||
|
string stairs = st.StairsConeVisible ? "VIS " : "CULL";
|
||||||
|
_out.WriteLine(FormattableString.Invariant(
|
||||||
|
$"step={st.Index,3} eye=({st.Eye.X:F2},{st.Eye.Y:F2},{st.Eye.Z:F2}) root={root} floodMain={st.FloodHasMainCell} stairs={stairs} outPolys={st.OutsidePolys} flood={st.FloodCells}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The vanish pin: the staircase static must stay cone-visible on every
|
||||||
|
/// ascent step (the gaze looks STRAIGHT AT it — no gaze excuse).
|
||||||
|
///
|
||||||
|
/// PINNED RED 2026-06-11 (skip carries the defect; un-skip with the fix):
|
||||||
|
/// steps 195–201 (eye z 126.9–127.3, the roof-lip band between the main
|
||||||
|
/// cell's ceiling at 126.8 and the roof aperture plane at ~127.2) resolve
|
||||||
|
/// OUTDOOR and the per-building exterior flood admits NOTHING (flood=1 =
|
||||||
|
/// the outdoor node alone) — the eye is above every side aperture's
|
||||||
|
/// useful view and ON/INSIDE the roof aperture's plane, so
|
||||||
|
/// BuildFromExterior's seed side-test/in-plane reject refuses every exit
|
||||||
|
/// portal. The tower interior never floods → the staircase (a 0x0107
|
||||||
|
/// static) has no views → culled while walkable, and the roof-lip cell
|
||||||
|
/// geometry flaps as the live eye bobs across the band's edges. Fix
|
||||||
|
/// needs the retail oracle: does retail keep the viewer cell INTERIOR
|
||||||
|
/// through this band (curr_cell keep-curr above open-top cells), or can
|
||||||
|
/// ConstructView(CBldPortal) seed with an in-plane eye?
|
||||||
|
/// </summary>
|
||||||
|
[Fact(Skip = "#119-residual: pins the roof-lip flood gap (steps 195-201) — un-skip with the fix; see the doc comment")]
|
||||||
|
public void TowerAscent_StaircaseStaysConeVisible_EveryStep()
|
||||||
|
{
|
||||||
|
var steps = RunAscent();
|
||||||
|
if (steps is null) return;
|
||||||
|
|
||||||
|
var failures = steps.FindAll(s => !s.StairsConeVisible);
|
||||||
|
if (failures.Count > 0)
|
||||||
|
{
|
||||||
|
_out.WriteLine($"--- {failures.Count} stairs-CULLED steps ---");
|
||||||
|
DumpSteps(failures);
|
||||||
|
}
|
||||||
|
Assert.True(failures.Count == 0,
|
||||||
|
$"{failures.Count}/{steps.Count} ascent steps cone-cull the staircase (first at step {(failures.Count > 0 ? failures[0].Index : -1)}) — see output");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The flap pin: along a smooth monotone ascent, the resolved root may
|
||||||
|
/// transition between cells/outdoor but must not PING-PONG (A→B→A within
|
||||||
|
/// three consecutive steps = a knife-edge decision the live camera jitter
|
||||||
|
/// turns into per-frame flapping).
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void TowerAscent_RootDoesNotPingPong()
|
||||||
|
{
|
||||||
|
var steps = RunAscent();
|
||||||
|
if (steps is null) return;
|
||||||
|
|
||||||
|
var flips = new List<int>();
|
||||||
|
for (int i = 2; i < steps.Count; i++)
|
||||||
|
{
|
||||||
|
uint a = steps[i - 2].RootCellId, b = steps[i - 1].RootCellId, c = steps[i].RootCellId;
|
||||||
|
if (a != b && b != c && a == c)
|
||||||
|
flips.Add(i - 1);
|
||||||
|
}
|
||||||
|
if (flips.Count > 0)
|
||||||
|
{
|
||||||
|
_out.WriteLine($"--- {flips.Count} root ping-pong steps ---");
|
||||||
|
DumpSteps(steps, s => flips.Contains(s.Index) || flips.Contains(s.Index - 1) || flips.Contains(s.Index + 1));
|
||||||
|
}
|
||||||
|
Assert.True(flips.Count == 0,
|
||||||
|
$"{flips.Count} root ping-pongs along a monotone ascent — knife-edge root decisions (see output)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Full per-step table for the investigation record.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Diagnostic_TowerAscent_PerStepTable()
|
||||||
|
{
|
||||||
|
var steps = RunAscent();
|
||||||
|
if (steps is null) return;
|
||||||
|
DumpSteps(steps);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue