// 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 // ------------------------------------------------------------------ /// /// 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. /// 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(), stabList: new List(), seenOutside: seenOutside, containmentBsp: null); /// /// EnvCell with an explicit stab list (used by FindVisibleChildCell tests). /// private static EnvCell MakeEnvCellWithStab(uint id, Vector3 min, Vector3 max, IReadOnlyList stabList, bool seenOutside = false) => new EnvCell( id, Matrix4x4.Identity, Matrix4x4.Identity, min, max, portals: new List(), 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 { 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()); graph.Add(root); var pointFarAway = new Vector3(999, 999, 999); // outside all cells var result = graph.FindVisibleChildCell(rootId, pointFarAway); Assert.Null(result); } }