acdream/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapter.cs
Erik 6caedf25f9 fix(render) Campaign FW1: keep the conformance DAT adapter behind the seam guard
RuntimeDatAccessArchitectureTests correctly flagged WalkWorldDatAdapter
taking raw DatCollection in production. The adapter is the conformance
harness world builder, so it moves into the test assembly; the FW3
production adapter will consume the content-owner seam. Hermetic suite
back to 6,687/0; adapter + conformance 10/10.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 10:24:51 +02:00

226 lines
8.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.

using System.Numerics;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Types;
using Environment = DatReaderWriter.DBObjs.Environment;
namespace AcDream.App.Tests.Rendering.Walk;
using AcDream.App.Rendering.Walk;
/// <summary>
/// Campaign FW1 — builds the walk's world model from real DAT data for the
/// CONFORMANCE HARNESS (it lives in the test assembly deliberately: raw
/// <c>DatCollection</c> access is fenced to the content-owner seam in
/// production by <c>RuntimeDatAccessArchitectureTests</c>; the FW3
/// production adapter will consume the seam instead). The library parses
/// everything the walk needs (survey 2026-08-30: PORT nodes with
/// <c>PortalRef{PolyId, PortalIndex}</c> in retail field order, cell
/// portal records with the PortalSide flag bit, building portals with stab
/// lists); this adapter is the previously missing consumer.
///
/// Conventions (retail structs; the trace fixtures are the referee):
/// <list type="bullet">
/// <item>portal_side = <c>(Flags &amp; 0x2) != 0</c> (PortalFlags.PortalSide),
/// matching <c>EnvCellLandblockBuild</c>'s production decode.</item>
/// <item>Portal-polygon planes use the first-three-vertices cross with
/// <c>d = dot(N, p0)</c> — the same formula the production visibility
/// cell build has always used against retail's InitCell side test.</item>
/// <item>A building's event id is its containing outdoor landcell:
/// low word = cellX·8 + cellY + 1 at 24 m pitch.</item>
/// </list>
/// </summary>
public static class WalkWorldDatAdapter
{
public static WalkCell? BuildCell(DatCollection dats, uint cellId)
{
if (dats.Get<EnvCell>(cellId) is not EnvCell envCell)
return null;
if (dats.Get<Environment>(0x0D000000u | envCell.EnvironmentId) is not Environment environment
|| !environment.Cells.TryGetValue(envCell.CellStructure, out CellStruct? cellStruct)
|| cellStruct is null)
{
return null;
}
uint lbMask = cellId & 0xFFFF0000u;
int portalCount = envCell.CellPortals.Count;
var portals = new WalkCellPortal[portalCount];
var polygons = new WalkPolygon[portalCount];
for (int i = 0; i < portalCount; i++)
{
CellPortal portal = envCell.CellPortals[i];
portals[i] = new WalkCellPortal
{
OtherCellId = portal.OtherCellId == 0xFFFF
? 0xFFFFFFFFu
: lbMask | portal.OtherCellId,
PolygonIndex = i,
PortalSide = ((ushort)portal.Flags & 0x2) != 0 ? 1 : 0,
ExactMatch = ((ushort)portal.Flags & 0x1) != 0,
OtherPortalId = unchecked((short)portal.OtherPortalId),
};
polygons[i] = BuildPolygon(cellStruct, portal.PolygonId)
?? new WalkPolygon();
}
Matrix4x4 worldTransform =
Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation)
* Matrix4x4.CreateTranslation(
envCell.Position.Origin.X,
envCell.Position.Origin.Y,
envCell.Position.Origin.Z);
Matrix4x4.Invert(worldTransform, out Matrix4x4 inverse);
return new WalkCell
{
CellId = cellId,
Portals = portals,
PortalPolygons = polygons,
StabList = envCell.VisibleCells.Select(v => lbMask | v).ToArray(),
WorldTransform = worldTransform,
InverseWorldTransform = inverse,
};
}
public static Dictionary<uint, WalkCell> BuildInteriorCells(
DatCollection dats, uint landblockId)
{
uint lbMask = landblockId & 0xFFFF0000u;
var cells = new Dictionary<uint, WalkCell>();
for (uint low = 0x0100; low <= 0xFFFD; low++)
{
WalkCell? cell = BuildCell(dats, lbMask | low);
if (cell is null) break; // interior cells are contiguous from 0x0100
cells[cell.CellId] = cell;
}
return cells;
}
/// <summary>Buildings of one landblock, with their walk event ids
/// (containing landcell), portal records, and drawing-BSP portal trees.</summary>
public static List<WalkBuilding> BuildBuildings(DatCollection dats, uint landblockId)
{
uint lbMask = landblockId & 0xFFFF0000u;
var result = new List<WalkBuilding>();
if (dats.Get<LandBlockInfo>(lbMask | 0xFFFEu) is not LandBlockInfo info
|| info.Buildings is null)
{
return result;
}
foreach (BuildingInfo buildingInfo in info.Buildings)
{
Vector3 origin = new(
buildingInfo.Frame.Origin.X,
buildingInfo.Frame.Origin.Y,
buildingInfo.Frame.Origin.Z);
int cellX = (int)MathF.Floor(origin.X / 24f);
int cellY = (int)MathF.Floor(origin.Y / 24f);
uint positionCellId = lbMask | (uint)(cellX * 8 + cellY + 1);
var portals = new WalkBldPortal[buildingInfo.Portals.Count];
for (int i = 0; i < portals.Length; i++)
{
BuildingPortal portal = buildingInfo.Portals[i];
portals[i] = new WalkBldPortal
{
PortalSide = ((ushort)portal.Flags & 0x2) != 0 ? 1 : 0,
ExactMatch = ((ushort)portal.Flags & 0x1) != 0,
OtherCellId = portal.OtherCellId == 0xFFFF
? 0xFFFFFFFFu
: lbMask | portal.OtherCellId,
OtherPortalId = unchecked((short)portal.OtherPortalId),
StabList = portal.StabList.Select(s => lbMask | s).ToArray(),
};
}
WalkBspNode? bsp = null;
if (dats.Get<GfxObj>(buildingInfo.ModelId) is GfxObj gfxObj)
bsp = ConvertDrawingBsp(gfxObj, gfxObj.DrawingBSP?.Root);
result.Add(new WalkBuilding
{
PositionCellId = positionCellId,
Portals = portals,
DrawingBsp = bsp,
});
}
return result;
}
internal static WalkBspNode? ConvertDrawingBsp(GfxObj gfxObj, DrawingBSPNode? node)
{
if (node is null) return null;
var converted = new WalkBspNode
{
SplittingPlane = new WalkPlane(
node.SplittingPlane.Normal, node.SplittingPlane.D),
IsFail = node.Type == DatReaderWriter.Enums.BSPNodeType.Leaf,
PosNode = ConvertDrawingBsp(gfxObj, node.PosNode),
NegNode = ConvertDrawingBsp(gfxObj, node.NegNode),
};
if (node.Type == DatReaderWriter.Enums.BSPNodeType.Portal && node.Portals is not null)
{
var refs = new List<WalkPortalRef>(node.Portals.Count);
foreach (PortalRef portalRef in node.Portals)
{
WalkPolygon? polygon = BuildGfxPolygon(gfxObj, portalRef.PolyId);
if (polygon is not null)
refs.Add(new WalkPortalRef
{
PortalIndex = portalRef.PortalIndex,
Polygon = polygon,
});
}
converted.InPortals = refs.ToArray();
}
return converted;
}
private static WalkPolygon? BuildPolygon(CellStruct cellStruct, ushort polygonId)
{
if (!cellStruct.Polygons.TryGetValue(polygonId, out Polygon? poly)
|| poly is null || poly.VertexIds.Count < 3)
{
return null;
}
return BuildPolygonFromVertices(
poly.VertexIds,
id => cellStruct.VertexArray.Vertices.TryGetValue((ushort)id, out SWVertex? v)
? new Vector3(v.Origin.X, v.Origin.Y, v.Origin.Z)
: null);
}
private static WalkPolygon? BuildGfxPolygon(GfxObj gfxObj, ushort polygonId)
{
if (!gfxObj.Polygons.TryGetValue(polygonId, out Polygon? poly)
|| poly is null || poly.VertexIds.Count < 3)
{
return null;
}
return BuildPolygonFromVertices(
poly.VertexIds,
id => gfxObj.VertexArray.Vertices.TryGetValue((ushort)id, out SWVertex? v)
? new Vector3(v.Origin.X, v.Origin.Y, v.Origin.Z)
: null);
}
private static WalkPolygon? BuildPolygonFromVertices(
IReadOnlyList<short> vertexIds, Func<short, Vector3?> resolve)
{
var vertices = new Vector3[vertexIds.Count];
for (int i = 0; i < vertexIds.Count; i++)
{
Vector3? v = resolve(vertexIds[i]);
if (v is null) return null;
vertices[i] = v.Value;
}
Vector3 normal = Vector3.Normalize(
Vector3.Cross(vertices[1] - vertices[0], vertices[2] - vertices[0]));
return new WalkPolygon
{
Vertices = vertices,
Plane = new WalkPlane(normal, -Vector3.Dot(normal, vertices[0])),
};
}
}