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>
226 lines
8.9 KiB
C#
226 lines
8.9 KiB
C#
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 & 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])),
|
||
};
|
||
}
|
||
}
|