acdream/tests/AcDream.App.Tests/Rendering/CellVisibilityFromRootTests.cs
Erik 6a1fbbd44e refactor(render): Stage 3 T3.1 — delete FindCameraCell AABB grace-frame fallback
ComputeVisibilityFromRoot(null, …) now returns null (outdoor root) instead of
calling FindCameraCell(fallbackPos). Retail CellManager::ChangePosition
(0x004559B0) reads the transition-owned curr_cell — it does NOT re-derive from
a static position. W2a guarantees CurrCell is set from the first tick, so the
AABB fallback is dead. Deleted: FindCameraCell (389–446), _lastCameraCell,
_cellSwitchGraceFrames, CellSwitchGraceFrameCount. GetVisibleCells retains a
brute-force AABB scan for test-compat; ComputeVisibility stays for the same
reason. Updated 3 null-root tests in CellVisibilityFromRootTests to assert the
new null-returns-null behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 15:36:47 +02:00

143 lines
5.2 KiB
C#

// CellVisibilityFromRootTests.cs — UCG W2 Task 2 + Stage 3: tests for
// CellVisibility.ComputeVisibilityFromRoot.
//
// Acceptance criteria (Stage 3 — W2 null-fallback deleted):
// (a) ComputeVisibilityFromRoot(null, pos) returns NULL (outdoor root), regardless
// of whether any cells are registered. The AABB FindCameraCell fallback is gone.
// (b) ComputeVisibilityFromRoot(root, pos) with a registered root returns
// a result whose CameraCell is that root, regardless of whether 'pos'
// is geometrically inside it.
//
// CellVisibility is intentionally free of GL types — it can be unit-tested
// without a GPU context (confirmed: only System.Numerics dependency).
using System.Numerics;
using AcDream.App.Rendering;
using Xunit;
namespace AcDream.App.Tests.Rendering;
public class CellVisibilityFromRootTests
{
// ------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------
/// <summary>
/// Build a minimal LoadedCell with an axis-aligned bounding box and identity
/// transform so PointInCell works for a position inside the box.
/// </summary>
private static LoadedCell MakeCell(uint cellId, Vector3 boundsMin, Vector3 boundsMax)
{
return new LoadedCell
{
CellId = cellId,
WorldTransform = Matrix4x4.Identity,
InverseWorldTransform = Matrix4x4.Identity,
LocalBoundsMin = boundsMin,
LocalBoundsMax = boundsMax,
Portals = new(),
ClipPlanes = new(),
PortalPolygons = new(),
};
}
// ------------------------------------------------------------------
// (a) Stage 3: null root → null (outdoor root), not a position fallback
// ------------------------------------------------------------------
[Fact]
public void ComputeVisibilityFromRoot_NullRoot_ReturnsNull_WhenCellExists()
{
// Stage 3: null root → outdoor root → null result, even when a cell covers the
// fallback position. Pre-Stage 3 this called FindCameraCell(pos); now the caller
// must supply the root (physics CellGraph.CurrCell). Retail: CellManager::ChangePosition
// reads the transition-owned curr_cell — it does not re-derive from a static position.
var cv = new CellVisibility();
var cell = MakeCell(0xA9B40101u, Vector3.Zero, new Vector3(10, 10, 10));
cv.AddCell(cell);
var pos = new Vector3(5, 5, 5); // inside the cell — null root overrides
var fromNull = cv.ComputeVisibilityFromRoot(null, pos);
// Stage 3: null root → null (outdoor root path).
Assert.Null(fromNull);
}
[Fact]
public void ComputeVisibilityFromRoot_NullRoot_NoCells_ReturnsNull()
{
// With no cells registered and null root: always null (outdoor root).
var cv = new CellVisibility();
var posOutdoors = new Vector3(100, 100, 100);
var fromNull = cv.ComputeVisibilityFromRoot(null, posOutdoors);
Assert.Null(fromNull);
}
[Fact]
public void ComputeVisibilityFromRoot_NullRoot_PositionOutsideAllCells_ReturnsNull()
{
// Cell exists but null root: always null regardless of position.
var cv = new CellVisibility();
var cell = MakeCell(0xA9B40102u, Vector3.Zero, new Vector3(5, 5, 5));
cv.AddCell(cell);
var posOutside = new Vector3(100, 100, 100);
var fromNull = cv.ComputeVisibilityFromRoot(null, posOutside);
Assert.Null(fromNull);
}
// ------------------------------------------------------------------
// (b) Supplied root is used as BFS root → CameraCell == root
// ------------------------------------------------------------------
[Fact]
public void ComputeVisibilityFromRoot_RegisteredRoot_CameraCellIsSuppliedRoot()
{
// Arrange: cell registered in CellVisibility.
var cv = new CellVisibility();
var cell = MakeCell(0xA9B40103u, Vector3.Zero, new Vector3(10, 10, 10));
cv.AddCell(cell);
// The position can be OUTSIDE the cell — physics already determined membership
// via BSP, we just trust that answer.
var posAnywhere = new Vector3(999, 999, 999);
// Act
var result = cv.ComputeVisibilityFromRoot(cell, posAnywhere);
// Assert: CameraCell is the supplied root.
Assert.NotNull(result);
Assert.Same(cell, result!.CameraCell);
}
[Fact]
public void ComputeVisibilityFromRoot_RegisteredRoot_IncludesRootInVisibleCells()
{
var cv = new CellVisibility();
var cell = MakeCell(0xA9B40104u, Vector3.Zero, new Vector3(10, 10, 10));
cv.AddCell(cell);
var result = cv.ComputeVisibilityFromRoot(cell, Vector3.Zero);
Assert.NotNull(result);
Assert.Contains(cell.CellId, result!.VisibleCellIds);
}
[Fact]
public void ComputeVisibilityFromRoot_RegisteredRoot_LastVisibilityResultUpdated()
{
var cv = new CellVisibility();
var cell = MakeCell(0xA9B40105u, Vector3.Zero, new Vector3(10, 10, 10));
cv.AddCell(cell);
var result = cv.ComputeVisibilityFromRoot(cell, Vector3.Zero);
Assert.Same(result, cv.LastVisibilityResult);
}
}