231 lines
9.9 KiB
C#
231 lines
9.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 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);
|
||
}
|
||
}
|