acdream/tests/AcDream.Core.Tests/Physics/CellarLipWedgeTests.cs
Erik 9fdf6a5d01 chore(p2): strip cellar-lip dispatch-trace probes after visual confirmation
The stale-footCenter fix (cc4590f) is visually confirmed: cellar ascent is
smooth, inn door still blocks, generic step-up still climbs. The residual
9/29 (0,-1,0)-sliding-normal records did NOT manifest in live play —
confirming they were buggy-trajectory artifacts.

Remove the temporary investigation scaffolding added for this trace:
- [fc-dispatch] probe in BSPQuery.FindCollisions
- [step-sphere-down] probe in BSPQuery.StepSphereDown
- CellarLipWedgeTests.Diagnostic_TraceRecordByIndex [Theory]

Kept: the fix, the Fix_StaleFootCenter_* regression guards, and the
DocumentsResidualWedge_* documents-the-bug test. Core suite 1317 pass /
4 fail (documented baseline) / 1 skip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 09:24:20 +02:00

502 lines
24 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using AcDream.Core.Physics;
using DatReaderWriter.Enums;
using DatReaderWriter.Types;
using Xunit;
namespace AcDream.Core.Tests.Physics;
/// <summary>
/// P2 cellar-LIP wedge (2026-06-04) — deterministic reproduction of the
/// "blocked at the last step" wedge at the Holtburg cottage cellar lip,
/// distinct from the earlier issue-#98 cellar (cells 0xA9B4014X).
///
/// <para>
/// Built from live captures this session:
/// <list type="bullet">
/// <item><b>Retail-connector trace</b> (CEnvCell::find_collisions) proved
/// retail's connector cell <c>0xA9B40175</c> NEVER blocks (2692 OK +
/// 94 Adjusted + 0 Collided + 0 Slid over ~85K samples).</item>
/// <item><b>acdream live capture</b> at the wedge: the player (r=0.48 body
/// sphere) is mid-climb at world Z=93.936, carried in the 0.364 m-tall
/// threshold slab <c>0xA9B40175</c>. Its 0.48 sphere grazes the slab's
/// X wall (local X=9; sphere at X=8.523 reaches 9.003 — a 3 mm graze)
/// → <c>StepSphereUp</c> → <c>DoStepUp</c> fails (no CP on the flat
/// cottage floor) → <c>StepUpSlide</c> returns <c>Collided</c> → the
/// per-cell collide returns Collided → wedge.</item>
/// </list>
/// </para>
///
/// <para>
/// Fixtures are real cell dumps (<c>ACDREAM_DUMP_CELLS</c>) of the three lip
/// cells: <c>0xA9B40171</c> (cottage floor), <c>0xA9B40174</c>, <c>0xA9B40175</c>
/// (threshold connector). The <c>BSP=null</c> hydration gap is bridged with a
/// synthetic single-leaf BSP, same as <see cref="CellarUpTrajectoryReplayTests"/>.
/// </para>
///
/// <para>
/// <b>RED status:</b> the climb-advance assertion is expected to FAIL while the
/// wedge exists (the player freezes at the threshold ~Z=93.94 instead of
/// reaching the cottage floor ~Z=94.48). When the fix lands it flips to GREEN.
/// </para>
/// </summary>
public class CellarLipWedgeTests
{
private const uint CottageFloorId = 0xA9B40171u; // cottage room floor
private const uint Connector74Id = 0xA9B40174u;
private const uint ThresholdId = 0xA9B40175u; // 0.364 m threshold slab
// Player physics from PlayerMovementController.cs (human, from Setup).
private const float SphereRadius = 0.48f;
private const float SphereHeight = 1.20f;
private const float StepUpHeight = 0.40f;
private const float StepDownHeight = 0.04f;
// The live-captured wedge state. The probe's [indoor-bsp] wpos is the
// FOOT-SPHERE CENTER (153.406, 9.754, 93.936); the engine body Position is
// the foot BOTTOM = center radius = Z 93.456. Player carried in the
// 0.364 m threshold slab 0xA9B40175, climbing the cellar stairs; observed
// motion at the lip is world Y (into the cottage).
private static readonly Vector3 WedgeSphereCenter = new(153.406f, 9.754f, 93.936f);
private static readonly Vector3 WedgeBodyPos =
new(153.406f, 9.754f, 93.936f - SphereRadius); // foot bottom Z=93.456
private static readonly Vector3 PerTickOffset = new(0f, -0.10f, 0f);
private const float CottageFloorZ = 94.00f;
private const float RestOnCottageZ = CottageFloorZ + SphereRadius; // ≈94.48
/// <summary>
/// Diagnostic: drive the player off the threshold toward the cottage and
/// dump the trajectory + indoor-BSP/step probes. Always passes; the
/// captured stdout shows exactly what the engine does each tick.
/// </summary>
[Fact]
public void Diagnostic_DriveOffThreshold_DumpTrajectory()
{
PhysicsDiagnostics.ProbeResolveEnabled = true;
PhysicsDiagnostics.ProbeIndoorBspEnabled = true;
var saved = Console.Out;
var sw = new StringWriter();
Console.SetOut(sw);
try
{
var engine = BuildEngineWithLipFixtures();
var body = BuildWedgeBody();
var traj = SimulateTicks(engine, body, ThresholdId, 4);
Console.SetOut(saved);
var probeLines = sw.ToString();
File.WriteAllText(
Path.Combine(Path.GetTempPath(), "lip-wedge-diag.log"),
"TRAJECTORY:\n" + string.Join("\n", traj.Select(p =>
$"tick={p.Tick} pos=({p.Position.X:F4},{p.Position.Y:F4},{p.Position.Z:F4}) " +
$"cell=0x{p.CellId:X8} onGround={p.IsOnGround} cpValid={p.CpValid}")) +
"\n\nPROBES:\n" + probeLines);
Assert.True(true);
}
finally
{
Console.SetOut(saved);
PhysicsDiagnostics.ProbeResolveEnabled = false;
PhysicsDiagnostics.ProbeIndoorBspEnabled = false;
}
}
/// <summary>
/// DOCUMENTS-THE-BUG (passes while the wedge exists; FAILS when the fix
/// lands). Seeded at the live wedge position (foot-sphere center Z=93.936,
/// carried in the 0.364 m threshold slab 0xA9B40175) and driven forward,
/// the player FREEZES — blocked by the slab's X side wall (poly normal
/// world (1,0,0)) — instead of advancing onto the cottage floor. Retail's
/// 0175 never blocks (live CEnvCell::find_collisions trace: 0 Collided/Slid).
///
/// <para>
/// <b>Faithfulness caveat:</b> the per-tick drive direction (world Y) is an
/// approximation — the exact <c>targetPos</c> would come from an
/// <c>ACDREAM_CAPTURE_RESOLVE</c> JSONL of the live wedge. The Y drive is
/// PARALLEL to the X wall, so it reproduces the block but may not be the
/// real climb path. A candidate fix (replacing the A6.P4 neg-poly
/// "return Collided" shortcut with retail's slide_sphere) did NOT clear this:
/// the slide returns Slid with offset=0 (displacement already along the
/// crease), the loop re-checks with gDelta≈0 → SlideSphere's
/// offset.LengthSquared&lt;ε → Collided branch → revert. The real fix needs a
/// faithful repro + the slide/loop-commit investigation (see the handoff
/// doc 2026-06-04-p2-cellar-lip-flatfloor-cp-handoff.md, Correction 2).
/// </para>
///
/// When the wedge is fixed the player advances and this assertion FAILS —
/// that is the signal to flip it to assert the climb.
/// </summary>
[Fact]
public void DocumentsWedge_PlayerFrozenAtThreshold_BlockedByMinusXWall()
{
var engine = BuildEngineWithLipFixtures();
var body = BuildWedgeBody();
var traj = SimulateTicks(engine, body, ThresholdId, 30);
var final = traj[^1];
float yAdvance = WedgeBodyPos.Y - final.Position.Y; // +ve = moved into cottage
float zRise = final.Position.Z - WedgeBodyPos.Z; // +ve = climbed
Assert.True(
yAdvance < 0.1f && zRise < 0.1f,
$"DOCUMENTS-THE-BUG: expected the player to be FROZEN at the threshold " +
$"(the X-wall wedge). Instead it advanced to " +
$"({final.Position.X:F3},{final.Position.Y:F3},{final.Position.Z:F3}) " +
$"after 30 ticks (yAdvance={yAdvance:F3}, zRise={zRise:F3}). If the wedge " +
$"fix landed, FLIP this assertion to require the climb " +
$"(Z≥{CottageFloorZ - 0.05f:F2}, yAdvance>0.5).");
}
// ───────────────────────────── helpers ─────────────────────────────
private static PhysicsBody BuildWedgeBody() => new()
{
Position = WedgeBodyPos, // foot bottom Z=93.456
Orientation = Quaternion.Identity,
// Best-effort grounded seed: a flat floor at foot level so the player
// starts "on the ground" mid-climb (the exact body-before state would
// come from an ACDREAM_CAPTURE_RESOLVE JSONL; this approximation puts
// the foot-sphere center at Z=93.936 — the live wedge — so the
// geometric X-wall full-hit fires).
ContactPlaneValid = true,
ContactPlane = new System.Numerics.Plane(0f, 0f, 1f, -WedgeBodyPos.Z),
ContactPlaneCellId = ThresholdId,
WalkablePolygonValid = true,
WalkablePlane = new System.Numerics.Plane(0f, 0f, 1f, -WedgeBodyPos.Z),
WalkableVertices = new[]
{
new Vector3(WedgeBodyPos.X - 1f, WedgeBodyPos.Y - 1f, WedgeBodyPos.Z),
new Vector3(WedgeBodyPos.X - 1f, WedgeBodyPos.Y + 1f, WedgeBodyPos.Z),
new Vector3(WedgeBodyPos.X + 1f, WedgeBodyPos.Y + 1f, WedgeBodyPos.Z),
new Vector3(WedgeBodyPos.X + 1f, WedgeBodyPos.Y - 1f, WedgeBodyPos.Z),
},
WalkableUp = Vector3.UnitZ,
TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable,
};
private static List<TrajPoint> SimulateTicks(
PhysicsEngine engine, PhysicsBody body, uint initialCellId, int tickCount)
{
uint cellId = initialCellId;
bool isOnGround = true;
var traj = new List<TrajPoint> { new(0, body.Position, cellId, isOnGround, body.ContactPlaneValid) };
for (int tick = 1; tick <= tickCount; tick++)
{
Vector3 target = body.Position + PerTickOffset;
var result = engine.ResolveWithTransition(
currentPos: body.Position,
targetPos: target,
cellId: cellId,
sphereRadius: SphereRadius,
sphereHeight: SphereHeight,
stepUpHeight: StepUpHeight,
stepDownHeight: StepDownHeight,
isOnGround: isOnGround,
body: body,
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
movingEntityId: 0);
body.Position = result.Position;
cellId = result.CellId;
isOnGround = result.IsOnGround;
traj.Add(new(tick, body.Position, cellId, isOnGround, body.ContactPlaneValid));
}
return traj;
}
private sealed record TrajPoint(int Tick, Vector3 Position, uint CellId, bool IsOnGround, bool CpValid);
// ─────────────────────── faithful live-wedge replay ───────────────────────
// Replays a captured wedge ResolveWithTransition call (exact currentPos /
// targetPos / body-before from ACDREAM_CAPTURE_RESOLVE at the live cellar
// lip) through the lip-cell engine. The live climb direction is X,+Y (the
// synthetic Y guess was backwards). 29 representative wedge records are in
// Fixtures/cellar-lip/wedge-records.jsonl.
private static readonly System.Text.Json.JsonSerializerOptions WedgeJsonOptions =
new() { IncludeFields = true, PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase };
private static List<ResolveCaptureRecord> LoadWedgeRecords()
{
var path = Path.Combine(FixtureDir, "wedge-records.jsonl");
Assert.True(File.Exists(path), $"Wedge fixture missing: {path}");
var list = new List<ResolveCaptureRecord>();
foreach (var line in File.ReadLines(path))
{
if (string.IsNullOrWhiteSpace(line)) continue;
list.Add(System.Text.Json.JsonSerializer.Deserialize<ResolveCaptureRecord>(line, WedgeJsonOptions)!);
}
return list;
}
private static PhysicsBody SeedBody(PhysicsBodySnapshot s) => new()
{
Position = s.Position,
Orientation = s.Orientation,
Velocity = s.Velocity,
Acceleration = s.Acceleration,
Omega = s.Omega,
GroundNormal = s.GroundNormal,
SlidingNormal = s.SlidingNormal,
ContactPlaneValid = s.ContactPlaneValid,
ContactPlane = s.ContactPlane,
ContactPlaneCellId = s.ContactPlaneCellId,
ContactPlaneIsWater = s.ContactPlaneIsWater,
WalkablePolygonValid = s.WalkablePolygonValid,
WalkablePlane = s.WalkablePlane,
WalkableVertices = s.WalkableVertices,
WalkableUp = s.WalkableUp,
Elasticity = s.Elasticity,
Friction = s.Friction,
State = (PhysicsStateFlags)s.State,
TransientState = (TransientStateFlags)s.TransientState,
LastUpdateTime = s.LastUpdateTime,
};
private static (Vector3 res, float requested, float advance) ReplayRecord(ResolveCaptureRecord rec)
{
var engine = BuildEngineWithLipFixtures();
var body = SeedBody(rec.BodyBefore!);
var result = engine.ResolveWithTransition(
currentPos: rec.Input.CurrentPos,
targetPos: rec.Input.TargetPos,
cellId: rec.Input.CellId,
sphereRadius: rec.Input.SphereRadius,
sphereHeight: rec.Input.SphereHeight,
stepUpHeight: rec.Input.StepUpHeight,
stepDownHeight: rec.Input.StepDownHeight,
isOnGround: rec.Input.IsOnGround,
body: body,
moverFlags: (ObjectInfoState)rec.Input.MoverFlags,
movingEntityId: rec.Input.MovingEntityId);
float requested = Vector3.Distance(rec.Input.CurrentPos, rec.Input.TargetPos);
float advance = Vector3.Distance(rec.Input.CurrentPos, result.Position);
return (result.Position, requested, advance);
}
/// <summary>
/// Diagnostic: replay every captured wedge record and report advance% — to
/// confirm the lip-cell engine reproduces the live stuck (0% advance) before
/// asserting a fix. Always passes; results in the message + %TEMP% file.
/// </summary>
[Fact]
public void Diagnostic_ReplayLiveWedgeRecords_Advance()
{
var recs = LoadWedgeRecords();
var lines = new List<string>();
foreach (var (rec, i) in recs.Select((r, i) => (r, i)))
{
if (rec.BodyBefore is null) continue;
var (res, req, adv) = ReplayRecord(rec);
var cpN = rec.BodyBefore.ContactPlane.Normal;
lines.Add($"#{i} cp=({cpN.X:F2},{cpN.Y:F2},{cpN.Z:F2}) req={req:F3} adv={adv:F3} ({(req>0?100*adv/req:0):F0}%) " +
$"cur=({rec.Input.CurrentPos.X:F2},{rec.Input.CurrentPos.Y:F2},{rec.Input.CurrentPos.Z:F2}) " +
$"tgt=({rec.Input.TargetPos.X:F2},{rec.Input.TargetPos.Y:F2},{rec.Input.TargetPos.Z:F2}) " +
$"res=({res.X:F2},{res.Y:F2},{res.Z:F2})");
}
File.WriteAllText(Path.Combine(Path.GetTempPath(), "lip-wedge-replay.log"), string.Join("\n", lines));
Assert.True(true, string.Join("\n", lines.Take(10)));
}
/// <summary>
/// Diagnostic: replay ONE floor-CP wedge record with the step-up + indoor
/// probes on, capturing why the step-up fails. Output to %TEMP%/lip-wedge-stepup.log.
/// </summary>
[Fact]
public void Diagnostic_ReplayFloorCpRecord_StepUpProbes()
{
var rec = LoadWedgeRecords().First(r => r.BodyBefore is not null
&& r.BodyBefore.ContactPlane.Normal.Z > 0.99f);
var saved = Console.Out;
var sw = new StringWriter();
PhysicsDiagnostics.ProbeIndoorBspEnabled = true;
PhysicsDiagnostics.ProbeStepWalkEnabled = true;
Environment.SetEnvironmentVariable("ACDREAM_DUMP_STEPUP", "1");
Console.SetOut(sw);
try
{
var (res, req, adv) = ReplayRecord(rec);
Console.SetOut(saved);
File.WriteAllText(Path.Combine(Path.GetTempPath(), "lip-wedge-stepup.log"),
$"record cur=({rec.Input.CurrentPos.X:F4},{rec.Input.CurrentPos.Y:F4},{rec.Input.CurrentPos.Z:F4}) " +
$"tgt=({rec.Input.TargetPos.X:F4},{rec.Input.TargetPos.Y:F4},{rec.Input.TargetPos.Z:F4}) " +
$"req={req:F3} adv={adv:F3} res=({res.X:F4},{res.Y:F4},{res.Z:F4})\n\n" + sw.ToString());
Assert.True(true);
}
finally
{
Console.SetOut(saved);
Environment.SetEnvironmentVariable("ACDREAM_DUMP_STEPUP", null);
PhysicsDiagnostics.ProbeIndoorBspEnabled = false;
PhysicsDiagnostics.ProbeStepWalkEnabled = false;
}
}
/// <summary>
/// FIX VALIDATION (2026-06-05) — the stale-footCenter fix in
/// <c>RunCheckOtherCellsAndAdvance</c>. Retail's <c>check_other_cells</c>
/// (acclient_2013_pseudo_c.txt:272735) re-collides the OTHER cells against the
/// LIVE <c>sphere_path.global_sphere</c> — i.e. AFTER the primary cell's
/// <c>insert_into_cell</c> may have moved the sphere via a successful
/// <c>step_sphere_up</c>. acdream captured the foot-sphere center BEFORE the
/// primary collide and reused that stale snapshot, so once the lip-riser
/// step_up climbed the foot onto the cottage floor, <c>check_other_cells</c>
/// still queried 0171 at the pre-climb (sunk, penetrating) position → the foot
/// spuriously near-missed the very floor it had climbed onto → a doomed second
/// step_up against the floor normal whose slide unwound the climb →
/// validate_transition reverted → 0% advance.
///
/// <para>
/// Pre-fix: 0/29 captured wedge records advanced. Post-fix: the ramp-climb
/// family (≈20/29) advances onto the cottage floor (Z≈94). This asserts a
/// representative ramp record (#9, cp Z=0.78, no sliding normal) now climbs.
/// </para>
/// </summary>
[Fact]
public void Fix_StaleFootCenter_RampRecordClimbsCottageFloor()
{
var rec = LoadWedgeRecords()[9];
var (res, requested, advance) = ReplayRecord(rec);
Assert.True(advance > 0.25f * requested && res.Z >= CottageFloorZ - 0.05f,
$"Expected ramp record #9 to climb onto the cottage floor after the " +
$"stale-footCenter fix. advance={advance:F3} (req={requested:F3}), " +
$"res=({res.X:F3},{res.Y:F3},{res.Z:F3}); want advance>0.25·req and Z≥{CottageFloorZ - 0.05f:F2}.");
}
/// <summary>
/// FIX REGRESSION GUARD (2026-06-05): the majority of captured wedge records
/// advance after the stale-footCenter fix. Pre-fix 0/29 → post-fix ≈20/29.
/// A drop here means <c>check_other_cells</c> is again querying other cells at
/// a stale pre-step_up position (the cellar-lip wedge regressed).
/// </summary>
[Fact]
public void Fix_StaleFootCenter_MajorityOfWedgeRecordsAdvance()
{
var recs = LoadWedgeRecords();
int advanced = 0, total = 0;
foreach (var rec in recs)
{
if (rec.BodyBefore is null) continue;
total++;
var (res, req, adv) = ReplayRecord(rec);
if (adv > 0.25f * req) advanced++;
}
Assert.True(advanced >= 18,
$"Expected ≥18 of {total} captured wedge records to advance >0.25·req " +
$"after the stale-footCenter fix; got {advanced}.");
}
/// <summary>
/// DOCUMENTS-THE-BUG (passes while a RESIDUAL wedge exists; flip when fixed).
/// Record #6 is a FLOOR-contact-plane record that ALSO carries a stale
/// <c>(0,-1,0)</c> sliding normal (the cottage south wall). The stale-footCenter
/// fix does NOT clear it: <c>AdjustOffset</c>'s slide-crease projects the
/// into-cottage +Y motion onto the floor×wall crease (the world X axis) and
/// ZEROES it before the sphere moves, so only the X residual survives → it
/// full-hits the slab's X wall → a step_up that fails on the flat floor (no CP)
/// → Collided → revert → 0% advance.
///
/// <para>
/// This is the SEPARATE "(0,-1,0) sliding-normal +Y-kill" family (7/29 records).
/// It is slide-recovery territory — explicitly OUT OF SCOPE for this pass per
/// the kickoff ("do not re-investigate ... slide") — and is suspected to be a
/// buggy-trajectory artifact (the stale slide accumulated only because the
/// player was already oscillating; once the ramp-climb advances cleanly the
/// player should not enter the south-wall-slide-into-doorway state). The visual
/// gate decides whether it needs a follow-up. If the residual is fixed, flip
/// this to require advance &gt; 0.25·requested.
/// </para>
/// </summary>
[Fact]
public void DocumentsResidualWedge_LiveFloorCp_SlidingNormalKillsPlusY()
{
var recs = LoadWedgeRecords();
var rec = recs.First(r => r.BodyBefore is not null
&& r.BodyBefore.ContactPlane.Normal.Z > 0.99f);
var (res, requested, advance) = ReplayRecord(rec);
var c = rec.Input.CurrentPos; var t = rec.Input.TargetPos;
Assert.True(advance < 0.1f * requested,
$"DOCUMENTS-RESIDUAL: expected the player STUCK (sliding-normal +Y-kill). " +
$"Instead it advanced: cur=({c.X:F3},{c.Y:F3},{c.Z:F3}) tgt=({t.X:F3},{t.Y:F3},{t.Z:F3}) " +
$"res=({res.X:F3},{res.Y:F3},{res.Z:F3}) requested={requested:F3} advance={advance:F3}. " +
$"If the slide +Y-kill residual is fixed, FLIP this to require advance>0.25·requested.");
}
private static PhysicsEngine BuildEngineWithLipFixtures()
{
var cache = new PhysicsDataCache();
var engine = new PhysicsEngine { DataCache = cache };
foreach (var cellId in new[] { CottageFloorId, Connector74Id, ThresholdId })
{
var path = Path.Combine(FixtureDir, $"0x{cellId:X8}.json");
Assert.True(File.Exists(path), $"Lip fixture missing: {path}");
var dump = CellDumpSerializer.Read(path);
var cell = CellDumpSerializer.Hydrate(dump);
cache.RegisterCellStructForTest(cellId, AttachSyntheticBsp(cell));
}
// Empty-terrain landblock so FindObjCollisions' TryGetLandblockContext
// succeeds at the lip XY (X≈153, Y≈9). Flat far-below surface; the
// indoor BSP path fires first so terrain is never consulted.
var heights = new byte[81];
var heightTable = new float[256];
for (int i = 0; i < 256; i++) heightTable[i] = -1000f;
engine.AddLandblock(
landblockId: 0xA9B40000u,
terrain: new TerrainSurface(heights, heightTable),
cells: Array.Empty<CellSurface>(),
portals: Array.Empty<PortalPlane>(),
worldOffsetX: 0f,
worldOffsetY: 0f);
return engine;
}
private static CellPhysics AttachSyntheticBsp(CellPhysics cell)
{
var leaf = new PhysicsBSPNode
{
Type = BSPNodeType.Leaf,
BoundingSphere = new Sphere { Origin = new Vector3(0f, 0f, 0f), Radius = 15f },
};
foreach (var kv in cell.Resolved)
leaf.Polygons.Add(kv.Key);
return new CellPhysics
{
BSP = new PhysicsBSPTree { Root = leaf },
PhysicsPolygons = cell.PhysicsPolygons,
Vertices = cell.Vertices,
WorldTransform = cell.WorldTransform,
InverseWorldTransform = cell.InverseWorldTransform,
Resolved = cell.Resolved,
CellBSP = cell.CellBSP,
Portals = cell.Portals,
PortalPolygons = cell.PortalPolygons,
VisibleCellIds = cell.VisibleCellIds,
};
}
private static string FixtureDir =>
Path.Combine(SolutionRoot(), "tests", "AcDream.Core.Tests", "Fixtures", "cellar-lip");
private static string SolutionRoot()
{
var dir = AppContext.BaseDirectory;
while (!string.IsNullOrEmpty(dir))
{
if (File.Exists(Path.Combine(dir, "AcDream.slnx")))
return dir;
dir = Path.GetDirectoryName(dir);
}
throw new InvalidOperationException("Could not locate solution root (AcDream.slnx).");
}
}