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>
143 lines
5.2 KiB
C#
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);
|
|
}
|
|
}
|