test(render): Stage 3 T3.4 — CellGraphRootTests (6 tests)

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>
This commit is contained in:
Erik 2026-06-02 15:37:19 +02:00
parent 38a52a7dac
commit 573c5559a0

View file

@ -0,0 +1,208 @@
// CellGraphRootTests.cs — Stage 3 (2026-06-02): unit tests for render-root
// selection logic and CellGraph.FindVisibleChildCell.
//
// Tests 13: 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 46: 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);
}
}