New test file: tests/AcDream.Core.Tests/Rendering/CellGraphRootTests.cs. Tests 1-3: render-branch predicates (rootSeenOutside, playerInsideCell, renderSky): RootSelection_OutdoorRoot_NullCurrCell_SeenOutsideDefaultsToTrue RootSelection_BuildingInterior_SeenOutside_SkyRenderedAndSunKept RootSelection_Dungeon_NoSeenOutside_SkyNotRenderedAndSunZeroed Tests 4-6: CellGraph.FindVisibleChildCell: FindVisibleChildCell_PlayerCellContains_ReturnsPlayerCell FindVisibleChildCell_StabListContains_ReturnsNeighbour FindVisibleChildCell_NeitherContains_ReturnsNull All 6 pass. Core suite: 12 pre-existing failures (same baseline), 1276 passing. App suite: 160/160 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
208 lines
8.9 KiB
C#
208 lines
8.9 KiB
C#
// CellGraphRootTests.cs — Stage 3 (2026-06-02): unit tests for render-root
|
||
// selection logic and CellGraph.FindVisibleChildCell.
|
||
//
|
||
// Tests 1–3: render-branch predicates. These exercise the formulas in
|
||
// GameWindow.OnRender directly (Stage 3):
|
||
// rootSeenOutside = physicsRoot?.SeenOutside ?? true
|
||
// playerInsideCell = cameraInsideCell && !rootSeenOutside
|
||
// renderSky = !cameraInsideCell || rootSeenOutside
|
||
//
|
||
// Retail anchors:
|
||
// CellManager::ChangePosition @ 0x004559B0 (pseudo_c:94649) — landscape kept
|
||
// live iff seen_outside; sun zeroed when inside a sealed interior.
|
||
// SmartBox::RenderNormalMode @ 0x00453aa0 (pseudo_c:92635) — sky gate.
|
||
//
|
||
// Tests 4–6: CellGraph.FindVisibleChildCell (retail find_visible_child_cell
|
||
// @ 0x0052dc50, pseudo_c:311397).
|
||
|
||
using System.Collections.Generic;
|
||
using System.Numerics;
|
||
using AcDream.Core.World.Cells;
|
||
using Xunit;
|
||
|
||
namespace AcDream.Core.Tests.Rendering;
|
||
|
||
public class CellGraphRootTests
|
||
{
|
||
// ------------------------------------------------------------------
|
||
// Helpers
|
||
// ------------------------------------------------------------------
|
||
|
||
/// <summary>
|
||
/// Synthetic EnvCell with an identity transform and axis-aligned bounds so
|
||
/// PointInCell returns true for points inside [min, max].
|
||
/// seenOutside = false → sealed dungeon; true → building interior/exterior.
|
||
/// </summary>
|
||
private static EnvCell MakeEnvCell(uint id, Vector3 min, Vector3 max, bool seenOutside = false)
|
||
=> new EnvCell(
|
||
id,
|
||
Matrix4x4.Identity,
|
||
Matrix4x4.Identity,
|
||
min, max,
|
||
portals: new List<CellPortal>(),
|
||
stabList: new List<uint>(),
|
||
seenOutside: seenOutside,
|
||
containmentBsp: null);
|
||
|
||
/// <summary>
|
||
/// EnvCell with an explicit stab list (used by FindVisibleChildCell tests).
|
||
/// </summary>
|
||
private static EnvCell MakeEnvCellWithStab(uint id, Vector3 min, Vector3 max,
|
||
IReadOnlyList<uint> stabList, bool seenOutside = false)
|
||
=> new EnvCell(
|
||
id,
|
||
Matrix4x4.Identity,
|
||
Matrix4x4.Identity,
|
||
min, max,
|
||
portals: new List<CellPortal>(),
|
||
stabList: stabList,
|
||
seenOutside: seenOutside,
|
||
containmentBsp: null);
|
||
|
||
// ------------------------------------------------------------------
|
||
// Predicate helpers — mirror the formulas in GameWindow.OnRender (Stage 3)
|
||
// ------------------------------------------------------------------
|
||
|
||
private static bool RootSeenOutside(EnvCell? physicsRoot) => physicsRoot?.SeenOutside ?? true;
|
||
|
||
// Retail CellManager::ChangePosition (0x004559B0): sun/sun-ambient zeroed
|
||
// when inside a sealed interior (cameraInsideCell=true, seen_outside=false).
|
||
// Building interiors with seen_outside keep the sun; outdoor root always keeps it.
|
||
private static bool PlayerInsideCell(bool cameraInsideCell, bool rootSeenOutside)
|
||
=> cameraInsideCell && !rootSeenOutside;
|
||
|
||
// Stage 3 sky gate: visible unless inside a sealed dungeon.
|
||
private static bool RenderSky(bool cameraInsideCell, bool rootSeenOutside)
|
||
=> !cameraInsideCell || rootSeenOutside;
|
||
|
||
// ------------------------------------------------------------------
|
||
// Test 1: outdoor root (null CurrCell) — seen_outside=true, sky rendered,
|
||
// player NOT considered inside a cell.
|
||
// ------------------------------------------------------------------
|
||
|
||
[Fact]
|
||
public void RootSelection_OutdoorRoot_NullCurrCell_SeenOutsideDefaultsToTrue()
|
||
{
|
||
// When physicsRoot==null (pre-spawn, or outdoor landcell) the ?? true
|
||
// gives rootSeenOutside=true; cameraInsideCell=false (no cell).
|
||
bool rootSeenOutside = RootSeenOutside(null);
|
||
bool cameraInsideCell = false; // no indoor root → ComputeVisibilityFromRoot returns null
|
||
|
||
bool playerInsideCell = PlayerInsideCell(cameraInsideCell, rootSeenOutside);
|
||
bool renderSky = RenderSky(cameraInsideCell, rootSeenOutside);
|
||
|
||
Assert.True(rootSeenOutside, "outdoor null root → rootSeenOutside=true");
|
||
Assert.False(playerInsideCell, "outdoor → playerInsideCell=false (sun kept live)");
|
||
Assert.True(renderSky, "outdoor → sky rendered");
|
||
}
|
||
|
||
// ------------------------------------------------------------------
|
||
// Test 2: building interior (seen_outside=true) — sky rendered (interim
|
||
// full-screen until Stage 4 clips it), sun kept live.
|
||
// ------------------------------------------------------------------
|
||
|
||
[Fact]
|
||
public void RootSelection_BuildingInterior_SeenOutside_SkyRenderedAndSunKept()
|
||
{
|
||
// A cottage cell: seen_outside=true (exit portal exists).
|
||
var physicsRoot = MakeEnvCell(0xA9B40170u, Vector3.Zero, new Vector3(10, 10, 10),
|
||
seenOutside: true);
|
||
|
||
bool rootSeenOutside = RootSeenOutside(physicsRoot);
|
||
bool cameraInsideCell = true; // inside a cell → ComputeVisibilityFromRoot non-null
|
||
|
||
bool playerInsideCell = PlayerInsideCell(cameraInsideCell, rootSeenOutside);
|
||
bool renderSky = RenderSky(cameraInsideCell, rootSeenOutside);
|
||
|
||
Assert.True(rootSeenOutside, "building interior seen_outside=true");
|
||
Assert.False(playerInsideCell, "seen_outside=true → sun kept live");
|
||
Assert.True(renderSky, "seen_outside=true → sky rendered (Stage 4 will clip to doorway)");
|
||
}
|
||
|
||
// ------------------------------------------------------------------
|
||
// Test 3: sealed dungeon (seen_outside=false) — sky suppressed, sun zeroed.
|
||
// ------------------------------------------------------------------
|
||
|
||
[Fact]
|
||
public void RootSelection_Dungeon_NoSeenOutside_SkyNotRenderedAndSunZeroed()
|
||
{
|
||
// A dungeon cell: seen_outside=false (no exit portal reachable).
|
||
var physicsRoot = MakeEnvCell(0x01D90100u, Vector3.Zero, new Vector3(10, 10, 10),
|
||
seenOutside: false);
|
||
|
||
bool rootSeenOutside = RootSeenOutside(physicsRoot);
|
||
bool cameraInsideCell = true; // inside a cell → non-null visibility result
|
||
|
||
bool playerInsideCell = PlayerInsideCell(cameraInsideCell, rootSeenOutside);
|
||
bool renderSky = RenderSky(cameraInsideCell, rootSeenOutside);
|
||
|
||
Assert.False(rootSeenOutside, "dungeon seen_outside=false");
|
||
Assert.True(playerInsideCell, "sealed dungeon → playerInsideCell=true (sun zeroed)");
|
||
Assert.False(renderSky, "sealed dungeon → sky suppressed");
|
||
}
|
||
|
||
// ------------------------------------------------------------------
|
||
// Test 4: FindVisibleChildCell — query inside the root cell → returns root.
|
||
// ------------------------------------------------------------------
|
||
|
||
[Fact]
|
||
public void FindVisibleChildCell_PlayerCellContains_ReturnsPlayerCell()
|
||
{
|
||
var graph = new CellGraph();
|
||
var root = MakeEnvCell(0xA9B40170u, Vector3.Zero, new Vector3(10, 10, 10));
|
||
graph.Add(root);
|
||
|
||
var point = new Vector3(5, 5, 5); // inside root's bounds
|
||
var result = graph.FindVisibleChildCell(root.Id, point);
|
||
|
||
Assert.NotNull(result);
|
||
Assert.Equal(root.Id, result!.Id);
|
||
}
|
||
|
||
// ------------------------------------------------------------------
|
||
// Test 5: FindVisibleChildCell — query inside a stab-list neighbour → returns that cell.
|
||
// ------------------------------------------------------------------
|
||
|
||
[Fact]
|
||
public void FindVisibleChildCell_StabListContains_ReturnsNeighbour()
|
||
{
|
||
var graph = new CellGraph();
|
||
|
||
// Root at [0,10]. Neighbour B at [20,30]. Root's stab list includes B.
|
||
uint rootId = 0xA9B40170u;
|
||
uint neighId = 0xA9B40171u;
|
||
|
||
var neigh = MakeEnvCell(neighId, new Vector3(20, 0, 0), new Vector3(30, 10, 10));
|
||
var root = MakeEnvCellWithStab(rootId, Vector3.Zero, new Vector3(10, 10, 10),
|
||
stabList: new List<uint> { neighId });
|
||
|
||
graph.Add(root);
|
||
graph.Add(neigh);
|
||
|
||
var point = new Vector3(25, 5, 5); // inside neighbour, outside root
|
||
var result = graph.FindVisibleChildCell(rootId, point);
|
||
|
||
Assert.NotNull(result);
|
||
Assert.Equal(neighId, result!.Id);
|
||
}
|
||
|
||
// ------------------------------------------------------------------
|
||
// Test 6: FindVisibleChildCell — query outside all cells → null.
|
||
// ------------------------------------------------------------------
|
||
|
||
[Fact]
|
||
public void FindVisibleChildCell_NeitherContains_ReturnsNull()
|
||
{
|
||
var graph = new CellGraph();
|
||
uint rootId = 0xA9B40170u;
|
||
|
||
var root = MakeEnvCellWithStab(rootId, Vector3.Zero, new Vector3(10, 10, 10),
|
||
stabList: new List<uint>());
|
||
graph.Add(root);
|
||
|
||
var pointFarAway = new Vector3(999, 999, 999); // outside all cells
|
||
var result = graph.FindVisibleChildCell(rootId, pointFarAway);
|
||
|
||
Assert.Null(result);
|
||
}
|
||
}
|