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>
113 lines
5.3 KiB
C#
113 lines
5.3 KiB
C#
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),
|
||
};
|
||
}
|