Purely additive: creates the synthetic outdoor cell node that will serve as the flood-graph root for the unified render pipeline. Each nearby building's exit portal (OtherCellId==0xFFFF) is reversed into a portal pointing back into the building, with its polygon transformed to world space and InsideSide flipped so the outdoor half-space is "inside" the node. WorldTransform=Identity (portals in world space). Mirrors retail's outdoor landcell that DrawInside(viewer_cell) roots at (SmartBox::RenderNormalMode, decomp pc:92635). Nothing consumes this yet — consumer wiring is Task 2. 2 new tests, 212 total passing, 0 regressions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
52 lines
2.2 KiB
C#
52 lines
2.2 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Rendering;
|
|
using Xunit;
|
|
|
|
namespace AcDream.App.Tests.Rendering;
|
|
|
|
public class OutdoorCellNodeTests
|
|
{
|
|
// A building cell at world-translate (10,0,0) with one exit portal (OtherCellId=0xFFFF)
|
|
// whose local plane faces +X (InsideSide=0). The outdoor node must expose ONE portal
|
|
// back into that building cell, with the entrance polygon moved to world space and the
|
|
// inside-side flipped (so the outdoor half-space is "inside" the node).
|
|
private static LoadedCell BuildingWithOneExit(uint cellId)
|
|
{
|
|
var cell = new LoadedCell { CellId = cellId };
|
|
cell.WorldTransform = Matrix4x4.CreateTranslation(10f, 0f, 0f);
|
|
cell.InverseWorldTransform = Matrix4x4.CreateTranslation(-10f, 0f, 0f);
|
|
cell.Portals.Add(new CellPortalInfo(OtherCellId: 0xFFFF, PolygonId: 0, Flags: 0, OtherPortalId: 0));
|
|
cell.ClipPlanes.Add(new PortalClipPlane { Normal = new Vector3(1, 0, 0), D = 0f, InsideSide = 0 });
|
|
cell.PortalPolygons.Add(new[]
|
|
{
|
|
new Vector3(0, -1, 0), new Vector3(0, 1, 0), new Vector3(0, 1, 2), new Vector3(0, -1, 2)
|
|
});
|
|
return cell;
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_FromBuildingExit_AddsReversePortalIntoBuilding()
|
|
{
|
|
uint outdoorId = 0xA9B40031;
|
|
var building = BuildingWithOneExit(0xA9B40170);
|
|
var node = OutdoorCellNode.Build(outdoorId, new[] { building });
|
|
|
|
Assert.Equal(outdoorId, node.CellId);
|
|
Assert.True(node.SeenOutside);
|
|
Assert.Equal(Matrix4x4.Identity, node.WorldTransform);
|
|
Assert.Single(node.Portals);
|
|
Assert.Equal((ushort)(0xA9B40170 & 0xFFFF), node.Portals[0].OtherCellId);
|
|
// Reversed inside-side: the building's exit was InsideSide=0, the node's is 1.
|
|
Assert.Equal(1, node.ClipPlanes[0].InsideSide);
|
|
// Entrance polygon moved to world space (building translated +10 X): first vert x≈10.
|
|
Assert.Equal(10f, node.PortalPolygons[0][0].X, 3);
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_NoBuildings_ReturnsEmptyPortalNode()
|
|
{
|
|
var node = OutdoorCellNode.Build(0xA9B40031, System.Array.Empty<LoadedCell>());
|
|
Assert.Empty(node.Portals);
|
|
Assert.True(node.SeenOutside);
|
|
}
|
|
}
|