acdream/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapter.cs
Erik e5cdd2364e feat(render) Campaign FW1: look-in adjudication - the GetVisible load gate
The per-building join diagnostic PASSES (001a/0022 portal tables lead
exactly to the traced a9b4016x punch cells; every BSP PortalRef indexes
validly), and the eight-arm gate-decode sweep proves NO plane-sign x
side-flag combination reproduces retail. Together they pin the missing
mechanism: CEnvCell::GetVisible gates punches by the LOADED interior
cell set around the player - retail punched only the two buildings
nearest the player cell; the replay loads every interior so geometry
alone over-punches. Next port piece: the interior load radius (the
landcell stab-list pull). Both diagnostics stay in the suite (the sweep
Skip-parked with the verdict).

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

268 lines
11 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 = DecodeBuildingSide((ushort)portal.Flags),
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);
}
/// <summary>Adjudication toggle (2026-08-30): the GfxObj portal-poly
/// plane winding vs the cell convention is under test — the street
/// fixture shows retail punching far fewer look-ins than the first-3
/// cross convention admits.</summary>
public static bool FlipGfxPolygonPlanes;
/// <summary>Adjudication toggle: the BuildingPortal side-flag decode.
/// 0 = (Flags &amp; 0x2), 1 = inverted 0x2, 2 = (Flags &amp; 0x1),
/// 3 = inverted 0x1.</summary>
public static int BuildingSideMode;
private static int DecodeBuildingSide(ushort flags) => BuildingSideMode switch
{
0 => (flags & 0x2) != 0 ? 1 : 0,
1 => (flags & 0x2) != 0 ? 0 : 1,
2 => (flags & 0x1) != 0 ? 1 : 0,
_ => (flags & 0x1) != 0 ? 0 : 1,
};
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;
}
WalkPolygon? built = 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);
if (built is not null && FlipGfxPolygonPlanes)
built.Plane = new WalkPlane(-built.Plane.Normal, -built.Plane.D);
return built;
}
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])),
};
}
}