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>
111 lines
4.6 KiB
C#
111 lines
4.6 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>CPhysicsObj::AdjustPosition</c>
|
|
/// (<c>acclient_2013_pseudo_c.txt:280009</c>): resolve which cell actually
|
|
/// contains a world point, given a seed cell. Indoor (<c>objcell_id ≥ 0x100</c>)
|
|
/// delegates to <see cref="CellTransit.FindVisibleChildCell"/> (stab-list mode,
|
|
/// retail <c>arg5 = 1</c>); outdoor snaps to the landcell under the point
|
|
/// (retail <c>LandDefs::adjust_to_outside</c>). <c>SmartBox::update_viewer</c>
|
|
/// uses it to seat the camera sweep's start cell at the head-pivot, and again
|
|
/// as fallback 1 at the sought eye.
|
|
/// </summary>
|
|
public class PhysicsEngineAdjustPositionTests
|
|
{
|
|
private const uint FeetCellId = 0xA9B40174u; // indoor connector (the cellar lip)
|
|
private const uint RoomCellId = 0xA9B40171u; // indoor room above, in the feet cell's stab list
|
|
private const uint LandblockId = 0xA9B40000u;
|
|
|
|
[Fact]
|
|
public void Indoor_PivotInStabListSibling_ResolvesSiblingAndFound()
|
|
{
|
|
var engine = BuildIndoorEngine();
|
|
|
|
// Pivot at Y=8 is outside the feet cell (Y≤3) but inside the room cell (Y≥7).
|
|
var (cellId, found) = engine.AdjustPosition(FeetCellId, new Vector3(0f, 8f, 0f));
|
|
|
|
Assert.True(found);
|
|
Assert.Equal(RoomCellId, cellId);
|
|
}
|
|
|
|
[Fact]
|
|
public void Indoor_PointInNoCell_NotFound()
|
|
{
|
|
var engine = BuildIndoorEngine();
|
|
|
|
// Y=5 is in the gap between the feet cell (Y≤3) and the room cell (Y≥7).
|
|
var (_, found) = engine.AdjustPosition(FeetCellId, new Vector3(0f, 5f, 0f));
|
|
|
|
Assert.False(found);
|
|
}
|
|
|
|
[Fact]
|
|
public void Outdoor_PointInLandblock_SnapsToLandcell()
|
|
{
|
|
var engine = BuildIndoorEngine(); // also registers the landblock
|
|
|
|
// Outdoor seed (low byte < 0x100). A point inside the landblock snaps to a
|
|
// prefixed landcell with found=true (retail LandDefs::adjust_to_outside).
|
|
var (cellId, found) = engine.AdjustPosition(0xA9B40001u, new Vector3(12f, 12f, 50f));
|
|
|
|
Assert.True(found);
|
|
Assert.Equal(LandblockId, cellId & 0xFFFF0000u); // correct landblock prefix
|
|
Assert.True((cellId & 0xFFFFu) < 0x0100u); // an outdoor landcell low byte
|
|
}
|
|
|
|
// ── fixture ────────────────────────────────────────────────────────────
|
|
|
|
private static PhysicsEngine BuildIndoorEngine()
|
|
{
|
|
var cache = new PhysicsDataCache();
|
|
var engine = new PhysicsEngine { DataCache = cache };
|
|
|
|
cache.RegisterCellStructForTest(FeetCellId, MakeCell(InteriorYAtMost(3f), new uint[] { RoomCellId }));
|
|
cache.RegisterCellStructForTest(RoomCellId, MakeCell(InteriorYAtLeast(7f), Array.Empty<uint>()));
|
|
|
|
// A flat stub landblock so the outdoor branch has a grid to snap to.
|
|
var heights = new byte[81];
|
|
var heightTable = new float[256];
|
|
engine.AddLandblock(
|
|
landblockId: LandblockId,
|
|
terrain: new TerrainSurface(heights, heightTable),
|
|
cells: Array.Empty<CellSurface>(),
|
|
portals: Array.Empty<PortalPlane>(),
|
|
worldOffsetX: 0f,
|
|
worldOffsetY: 0f);
|
|
|
|
return engine;
|
|
}
|
|
|
|
private static CellBSPNode InteriorYAtMost(float boundary) => new()
|
|
{
|
|
SplittingPlane = new Plane(new Vector3(0f, -1f, 0f), boundary),
|
|
PosNode = new CellBSPNode { Type = BSPNodeType.Leaf },
|
|
};
|
|
|
|
private static CellBSPNode InteriorYAtLeast(float boundary) => new()
|
|
{
|
|
SplittingPlane = new Plane(new Vector3(0f, 1f, 0f), -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),
|
|
};
|
|
}
|