diff --git a/src/AcDream.App/Rendering/Walk/WalkWorldDatAdapter.cs b/src/AcDream.App/Rendering/Walk/WalkWorldDatAdapter.cs
new file mode 100644
index 00000000..767abf72
--- /dev/null
+++ b/src/AcDream.App/Rendering/Walk/WalkWorldDatAdapter.cs
@@ -0,0 +1,211 @@
+using System.Numerics;
+using DatReaderWriter;
+using DatReaderWriter.DBObjs;
+using DatReaderWriter.Types;
+using Environment = DatReaderWriter.DBObjs.Environment;
+
+namespace AcDream.App.Rendering.Walk;
+
+///
+/// Campaign FW1 — builds the walk's world model from real DAT data. 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
+{
+ public static WalkCell? BuildCell(DatCollection dats, uint cellId)
+ {
+ 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();
+ }
+
+ return new WalkCell
+ {
+ CellId = cellId,
+ Portals = portals,
+ PortalPolygons = polygons,
+ StabList = envCell.VisibleCells.Select(v => lbMask | v).ToArray(),
+ };
+ }
+
+ public static Dictionary BuildInteriorCells(
+ DatCollection dats, uint landblockId)
+ {
+ uint lbMask = landblockId & 0xFFFF0000u;
+ var cells = new Dictionary();
+ 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;
+ }
+
+ /// Buildings of one landblock, with their walk event ids
+ /// (containing landcell), portal records, and drawing-BSP portal trees.
+ 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 = ((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(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(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 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])),
+ };
+ }
+}
diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapterTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapterTests.cs
new file mode 100644
index 00000000..38fb831a
--- /dev/null
+++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapterTests.cs
@@ -0,0 +1,112 @@
+using AcDream.App.Tests.Rendering;
+using AcDream.App.Rendering.Walk;
+using DatReaderWriter;
+using DatReaderWriter.Options;
+
+namespace AcDream.App.Tests.Rendering.Walk;
+
+///
+/// Ground-truth conformance for the DAT adapter: every building the FW0
+/// oracle traces saw retail draw (the BLD event ids, captured live
+/// 2026-08-30) must exist in the adapter's roster for its landblock with
+/// the EXACT position cell id, and the look-in cells the traces name must
+/// build with portals and polygons. This validates the ModelId/Frame →
+/// landcell computation and the cell extraction against retail's own
+/// output — no camera pose required.
+///
+[Trait("Lane", "InstalledDat")]
+public sealed class WalkWorldDatAdapterTests
+{
+ private static DatCollection OpenDats()
+ {
+ string? datDir = CornerFloodReplayTests.ResolveDatDir();
+ if (datDir is null)
+ {
+ Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");
+ }
+ return new DatCollection(datDir!, DatAccessType.Read);
+ }
+
+ public static readonly TheoryData TraceBuildingRosters = new()
+ {
+ {
+ 0xF4180000u,
+ new[]
+ {
+ 0xF4180011u, 0xF4180001u, 0xF4180009u, 0xF4180002u, 0xF4180004u,
+ 0xF4180014u, 0xF418000Au, 0xF418000Cu, 0xF418000Bu,
+ }
+ },
+ { 0xF5180000u, new[] { 0xF518002Eu } },
+ { 0xF4170000u, new[] { 0xF4170011u, 0xF4170012u, 0xF4170002u } },
+ { 0xF3180000u, new[] { 0xF3180020u } },
+ {
+ 0xA9B40000u,
+ new[]
+ {
+ 0xA9B4000Fu, 0xA9B40017u, 0xA9B4002Fu, 0xA9B40016u, 0xA9B4001Eu,
+ 0xA9B40026u, 0xA9B40036u, 0xA9B4001Au, 0xA9B40022u, 0xA9B40032u,
+ 0xA9B40031u, 0xA9B40029u,
+ }
+ },
+ { 0xA9B30000u, new[] { 0xA9B3003Cu } },
+ { 0xAAB50000u, new[] { 0xAAB50002u } },
+ };
+
+ [Theory]
+ [MemberData(nameof(TraceBuildingRosters))]
+ public void Every_building_retail_drew_exists_at_its_exact_position_cell(
+ uint landblockId, uint[] tracedBuildingIds)
+ {
+ using DatCollection dats = OpenDats();
+
+ List buildings = WalkWorldDatAdapter.BuildBuildings(dats, landblockId);
+ var roster = buildings.Select(b => b.PositionCellId).ToHashSet();
+
+ foreach (uint traced in tracedBuildingIds)
+ Assert.Contains(traced, roster);
+ }
+
+ [Fact]
+ public void Traced_buildings_carry_portals_with_stab_lists_and_drawing_bsp_portals()
+ {
+ using DatCollection dats = OpenDats();
+
+ List buildings = WalkWorldDatAdapter.BuildBuildings(dats, 0xF4180000u);
+ WalkBuilding sanctuary = Assert.Single(
+ buildings, b => b.PositionCellId == 0xF418000Au);
+
+ Assert.NotEmpty(sanctuary.Portals);
+ Assert.All(sanctuary.Portals, p => Assert.NotEmpty(p.StabList));
+ // The look-in machinery needs PORT nodes in the drawing BSP.
+ Assert.NotNull(sanctuary.DrawingBsp);
+ Assert.True(
+ CountPortalRefs(sanctuary.DrawingBsp) > 0,
+ "the sanctuary building's drawing BSP must carry portal polygons");
+ }
+
+ [Fact]
+ public void The_traced_look_in_cells_build_with_portals_and_polygons()
+ {
+ using DatCollection dats = OpenDats();
+
+ // Cells named by the FW0 traces: the cathedral arrival root, the
+ // terrace cells, and the edge's extra look-in.
+ foreach (uint cellId in new[] { 0xF4180104u, 0xF4180106u, 0xF418010Fu })
+ {
+ WalkCell? cell = WalkWorldDatAdapter.BuildCell(dats, cellId);
+ Assert.NotNull(cell);
+ Assert.NotEmpty(cell!.Portals);
+ Assert.Equal(cell.Portals.Length, cell.PortalPolygons.Length);
+ Assert.All(cell.PortalPolygons, p => Assert.True(p.Vertices.Length >= 3));
+ Assert.NotEmpty(cell.StabList);
+ }
+ }
+
+ private static int CountPortalRefs(WalkBspNode? node)
+ {
+ if (node is null) return 0;
+ int count = node.InPortals?.Length ?? 0;
+ return count + CountPortalRefs(node.PosNode) + CountPortalRefs(node.NegNode);
+ }
+}