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])), }; } }