acdream/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapter.cs
Erik e3f0c6a1a1 feat(render) Campaign FW1: the landscape replay world + first outdoor diff
WalkLandscapeDatBuilder assembles the replay grid with the LIVE-dumped
retail landscape model (LScape mid_radius=25 -> 51x51; the resolution
pyramid observed on the capture client: side_cell_count 8 in the 3x3
core, 4 at ring 2, 2 at rings 3-4, 1 beyond; buildings attach only to
full-res blocks - matching the traces roster), retail z-slabs
(heightTable[max]+200 / [min]-1 per CLandBlock unpack @0052f297), and
camera-block-local coordinates. The replay context gains the building
half (placements, active-view install, building-polygon clip). The
street-outdoor conformance diff now reaches real adjudication: the walk
over-culls six traced buildings (jagged boundary - not a clean frustum
edge) and the look-in floods differ; the driving test is parked Skip
while the visibility map is instrumented.

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

246 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.

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
{
/// <summary>One built building plus its landblock-local placement.</summary>
public sealed record BuildingEntry(
WalkBuilding Building, Matrix4x4 WorldTransform, Matrix4x4 InverseWorldTransform);
public static WalkCell? BuildCell(DatCollection dats, uint cellId)
=> BuildCell(dats, cellId, Vector3.Zero);
public static WalkCell? BuildCell(DatCollection dats, uint cellId, Vector3 blockOffset)
{
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 + blockOffset.X,
envCell.Position.Origin.Y + blockOffset.Y,
envCell.Position.Origin.Z + blockOffset.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)
=> BuildInteriorCells(dats, landblockId, Vector3.Zero, null);
public static Dictionary<uint, WalkCell> BuildInteriorCells(
DatCollection dats, uint landblockId, Vector3 blockOffset,
Dictionary<uint, WalkCell>? into)
{
uint lbMask = landblockId & 0xFFFF0000u;
Dictionary<uint, WalkCell> cells = into ?? new Dictionary<uint, WalkCell>();
for (uint low = 0x0100; low <= 0xFFFD; low++)
{
WalkCell? cell = BuildCell(dats, lbMask | low, blockOffset);
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, drawing-BSP portal trees, and
/// landblock-local placements.</summary>
public static List<BuildingEntry> BuildBuildings(DatCollection dats, uint landblockId)
{
uint lbMask = landblockId & 0xFFFF0000u;
var result = new List<BuildingEntry>();
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);
Matrix4x4 worldTransform =
Matrix4x4.CreateFromQuaternion(buildingInfo.Frame.Orientation)
* Matrix4x4.CreateTranslation(origin);
Matrix4x4.Invert(worldTransform, out Matrix4x4 inverse);
result.Add(new BuildingEntry(
new WalkBuilding
{
PositionCellId = positionCellId,
Portals = portals,
DrawingBsp = bsp,
},
worldTransform,
inverse));
}
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])),
};
}
}