acdream/tests/AcDream.Core.Tests/Physics/CellTransitFindVisibleChildCellTests.cs
Erik 5177b54bbe feat(A): port find_visible_child_cell + AdjustPosition (Render Residual A primitives)
The two Core physics primitives retail's SmartBox::update_viewer calls down into,
ported verbatim (TDD, 7 new tests):

- CellTransit.FindVisibleChildCell (CEnvCell::find_visible_child_cell, pc:311397):
  return the cell whose cell-BSP point_in_cell contains a world point — start cell
  first, then (stab-list mode) the start's VisibleCellIds or (portal mode) its
  direct portals. Sibling of FindCellList. Mirrors FindCellList's null-CellBSP skip
  (CellTransit.cs:518) so a cell lacking hydrated CellBSP doesn't spuriously claim
  every point via PointInsideCellBsp's null-node "inside" default.

- PhysicsEngine.AdjustPosition (CPhysicsObj::AdjustPosition, pc:280009): resolve a
  point's cell from a seed. Indoor (>=0x100) → FindVisibleChildCell(stab-list);
  outdoor → landcell snap (same grid lookup as ResolveCellId). The seen_outside
  sub-fallback is deferred (off the cottage/cellar path; spec §6).

Both are unwired into any production path — they land the machinery update_viewer's
start-cell + fallback 1 need (and that residual C also needs). The App SweepEye
orchestration that calls them lands next.

Decomp-faithful per the live-capture finding: A's V1 sweep already contains the eye
(eyeInRoot=Y 99.75%, never void); this completes A as a verbatim port. Spec:
docs/superpowers/specs/2026-06-05-residual-a-camera-collision-design.md.

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

113 lines
5.3 KiB
C#
Raw 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.Numerics;
using AcDream.Core.Physics;
using DatReaderWriter.Enums;
using DatReaderWriter.Types;
using Xunit;
namespace AcDream.Core.Tests.Physics;
/// <summary>
/// Render Residual A — verbatim port of <c>CEnvCell::find_visible_child_cell</c>
/// (<c>acclient_2013_pseudo_c.txt:311397</c>): given a start cell, a world point,
/// and a mode, return the cell whose cell-BSP <c>point_in_cell</c> contains the
/// point — checking the start cell itself, then (stab-list mode) the start's
/// <c>VisibleCellIds</c> or (portal mode) its direct portal neighbours.
///
/// <para>
/// This is the sibling of <see cref="CellTransit.FindCellList"/> (retail
/// <c>find_cell_list</c>); both resolve cell membership from the cell graph. The
/// camera's <c>SmartBox::update_viewer</c> start-cell uses the stab-list mode
/// (<c>AdjustPosition</c> at pc:280028 passes <c>arg5=1</c>) to seat the sweep at
/// the PIVOT's cell, which differs from the feet cell at a low connector (the
/// cellar lip), where the pivot is up at floor level in a different cell.
/// </para>
///
/// <para>
/// Geometry is identity-transform (cell-local == world) so the synthetic CellBSP
/// splitting planes read directly: cell A is the half-space Y≤3, cell B (in A's
/// stab list) is the half-space Y≥7, and Y∈(3,7) belongs to neither.
/// </para>
/// </summary>
public class CellTransitFindVisibleChildCellTests
{
private const uint StartCellId = 0xA9B40174u; // low 0x0174 ≥ 0x0100 → indoor
private const uint SiblingCellId = 0xA9B40171u; // the "room above" in StartCell's stab list
[Fact]
public void PointInsideStartCell_ReturnsStartCell()
{
var cache = new PhysicsDataCache();
cache.RegisterCellStructForTest(StartCellId, MakeCell(InteriorYAtMost(3f), new uint[] { SiblingCellId }));
cache.RegisterCellStructForTest(SiblingCellId, MakeCell(InteriorYAtLeast(7f), Array.Empty<uint>()));
// P at Y=1 is inside A (Y≤3) → the "this" branch returns the start cell.
uint result = CellTransit.FindVisibleChildCell(cache, StartCellId, new Vector3(0f, 1f, 0f), useStabList: true);
Assert.Equal(StartCellId, result);
}
[Fact]
public void PointInStabListSibling_ReturnsSibling()
{
var cache = new PhysicsDataCache();
cache.RegisterCellStructForTest(StartCellId, MakeCell(InteriorYAtMost(3f), new uint[] { SiblingCellId }));
cache.RegisterCellStructForTest(SiblingCellId, MakeCell(InteriorYAtLeast(7f), Array.Empty<uint>()));
// P at Y=8 is outside A (Y≤3) but inside B (Y≥7), and B is in A's stab list.
uint result = CellTransit.FindVisibleChildCell(cache, StartCellId, new Vector3(0f, 8f, 0f), useStabList: true);
Assert.Equal(SiblingCellId, result);
}
[Fact]
public void PointInNoCell_ReturnsZero()
{
var cache = new PhysicsDataCache();
cache.RegisterCellStructForTest(StartCellId, MakeCell(InteriorYAtMost(3f), new uint[] { SiblingCellId }));
cache.RegisterCellStructForTest(SiblingCellId, MakeCell(InteriorYAtLeast(7f), Array.Empty<uint>()));
// P at Y=5 is in the gap: outside A (Y≤3) and outside B (Y≥7).
uint result = CellTransit.FindVisibleChildCell(cache, StartCellId, new Vector3(0f, 5f, 0f), useStabList: true);
Assert.Equal(0u, result);
}
[Fact]
public void UnknownStartCell_ReturnsZero()
{
var cache = new PhysicsDataCache();
uint result = CellTransit.FindVisibleChildCell(cache, 0xDEADBEEFu, new Vector3(0f, 1f, 0f), useStabList: true);
Assert.Equal(0u, result);
}
// ── helpers ─────────────────────────────────────────────────────────────
/// <summary>CellBSP root for the half-space Y ≤ <paramref name="boundary"/>
/// (interior on the Y side; <c>point_in_cell</c> true when Y ≤ boundary).</summary>
private static CellBSPNode InteriorYAtMost(float boundary) => new()
{
SplittingPlane = new Plane(new Vector3(0f, -1f, 0f), boundary), // dist = boundary Y ≥ 0 ⇔ Y ≤ boundary
PosNode = new CellBSPNode { Type = BSPNodeType.Leaf },
};
/// <summary>CellBSP root for the half-space Y ≥ <paramref name="boundary"/>.</summary>
private static CellBSPNode InteriorYAtLeast(float boundary) => new()
{
SplittingPlane = new Plane(new Vector3(0f, 1f, 0f), -boundary), // dist = Y boundary ≥ 0 ⇔ Y ≥ boundary
PosNode = new CellBSPNode { Type = BSPNodeType.Leaf },
};
private static CellPhysics MakeCell(CellBSPNode cellBspRoot, uint[] visibleCellIds) => new()
{
BSP = new PhysicsBSPTree { Root = new PhysicsBSPNode { Type = BSPNodeType.Leaf } },
WorldTransform = Matrix4x4.Identity,
InverseWorldTransform = Matrix4x4.Identity,
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
CellBSP = new CellBSPTree { Root = cellBspRoot },
Portals = Array.Empty<PortalInfo>(),
PortalPolygons = new Dictionary<ushort, ResolvedPolygon>(),
VisibleCellIds = new HashSet<uint>(visibleCellIds),
};
}