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;
///
/// Campaign FW1 — builds the walk's world model from real DAT data for the
/// CONFORMANCE HARNESS (it lives in the test assembly deliberately: raw
/// DatCollection access is fenced to the content-owner seam in
/// production by RuntimeDatAccessArchitectureTests; the FW3
/// production adapter will consume the seam instead). The library parses
/// everything the walk needs (survey 2026-08-30: PORT nodes with
/// PortalRef{PolyId, PortalIndex} 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):
///
/// - portal_side = (Flags & 0x2) != 0 (PortalFlags.PortalSide),
/// matching EnvCellLandblockBuild's production decode.
/// - Portal-polygon planes use the first-three-vertices cross with
/// d = −dot(N, p0) — the same formula the production visibility
/// cell build has always used against retail's InitCell side test.
/// - A building's event id is its containing outdoor landcell:
/// low word = cellX·8 + cellY + 1 at 24 m pitch.
///
///
public static class WalkWorldDatAdapter
{
/// One built building plus its landblock-local placement.
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(cellId) is not EnvCell envCell)
return null;
if (dats.Get(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 BuildInteriorCells(
DatCollection dats, uint landblockId)
=> BuildInteriorCells(dats, landblockId, Vector3.Zero, null);
public static Dictionary BuildInteriorCells(
DatCollection dats, uint landblockId, Vector3 blockOffset,
Dictionary? into)
{
uint lbMask = landblockId & 0xFFFF0000u;
Dictionary cells = into ?? new Dictionary();
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;
}
/// Buildings of one landblock, with their walk event ids
/// (containing landcell), portal records, drawing-BSP portal trees, and
/// landblock-local placements.
public static List BuildBuildings(DatCollection dats, uint landblockId)
{
uint lbMask = landblockId & 0xFFFF0000u;
var result = new List();
if (dats.Get(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(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(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);
}
/// 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.
public static bool FlipGfxPolygonPlanes;
/// Adjudication toggle: the BuildingPortal side-flag decode.
/// 0 = (Flags & 0x2), 1 = inverted 0x2, 2 = (Flags & 0x1),
/// 3 = inverted 0x1.
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 vertexIds, Func 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])),
};
}
}