acdream/tests/AcDream.Core.Tests/Rendering/CellGraphRootTests.cs

231 lines
9.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 DatReaderWriter.Enums;
using Xunit;
using CellBSPNode = DatReaderWriter.Types.CellBSPNode;
using CellBSPTree = DatReaderWriter.Types.CellBSPTree;
namespace AcDream.Core.Tests.Rendering;
public class CellGraphRootTests
{
// ------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------
/// <summary>
/// Synthetic EnvCell with an authored six-plane containment BSP for
/// [min,max] and one portal so retail CEnvCell::point_in_cell is eligible.
/// 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> { new(0xFFFFu, 0, 0, 0) },
stabList: new List<uint>(),
seenOutside: seenOutside,
containmentBsp: new CellBSPTree { Root = BoundsBsp(min, max) });
/// <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> { new(0xFFFFu, 0, 0, 0) },
stabList: stabList,
seenOutside: seenOutside,
containmentBsp: new CellBSPTree { Root = BoundsBsp(min, max) });
private static CellBSPNode BoundsBsp(Vector3 min, Vector3 max)
{
var leaf = new CellBSPNode { Type = BSPNodeType.Leaf };
CellBSPNode Add(Plane plane, CellBSPNode positive) => new()
{
Type = BSPNodeType.BPIn,
SplittingPlane = plane,
PosNode = positive,
};
CellBSPNode root = leaf;
root = Add(new Plane(-Vector3.UnitZ, max.Z), root);
root = Add(new Plane(Vector3.UnitZ, -min.Z), root);
root = Add(new Plane(-Vector3.UnitY, max.Y), root);
root = Add(new Plane(Vector3.UnitY, -min.Y), root);
root = Add(new Plane(-Vector3.UnitX, max.X), root);
root = Add(new Plane(Vector3.UnitX, -min.X), root);
return root;
}
// ------------------------------------------------------------------
// 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);
}
}