feat(render) Campaign FW3.1: production walk world data behind the seam

The retail frame walk's world model now materializes from production
landblock-build owners through the legal IDatReaderWriter seam, with
zero frame wiring (FW3.2 roots the frame):

- WalkCellFactory: WalkCell built in the SAME pass as LoadedCell
  (EnvCellLandblockBuild.BuildVisibilityCell) from the raw portal
  Flags/polygons/planes/stab lists already parsed there; stored as
  LoadedCell.Walk, committed atomically with the cell. The
  fixture-pinned decodes (inverse-0x2 portal side, 0xFFFF->0xFFFFFFFF
  exit widening) live here.
- WalkBuildingFactory + WalkBuildingRegistry: the production
  WalkBuilding build (drawing BSP with PORT nodes, degrade ladder,
  portal sides/stab lists, sort center, model frame) from the SAME
  LandBlockInfo the streaming build already fetches, under the
  factory's existing DAT lock - closing the gap where BuildingLoader
  drops every walk field at load.
- WalkLandscapeAssembler: the retail 51x51 viewer-centred grid
  (mid_radius 25) fed incrementally from landblock publish/retire;
  per-block z-slab (heightTable[max]+200 / [min]-1) computed
  worker-side in LandblockBuildFactory from the heights already in
  hand. O(1) SetViewer on same-block frames.
- WalkProductionFrameContext: the walk's frame contexts over
  CellVisibility + WalkBuildingRegistry with a generic
  inverse-view-projection ray caster (rays feed cross products only -
  scale-free) and the znear=0.1 CY plane.
- Publication: LandblockRenderPublisher owns both walk registries,
  publishing in the same AdvanceCompleteOne step as BuildingRegistry
  and retiring in RemoveBuildingRegistry - same commit, same
  retirement, no new ticket stage.

Conformance: ALL TEN oracle fixtures replay identically through the
PRODUCTION builders (WalkProductionWorldConformanceTests) - same
signatures as the test adapter, first run. Known gap documented for
FW3.2: far-tier landblocks carry no EnvCell transaction, so their
z-slab never reaches the assembler.

Suites: full Release build 0 warnings; Walk lane 186/1 skip;
hermetic 6,738/0 (+24); RuntimeDatAccessArchitectureTests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-30 13:19:22 +02:00
parent b95850defe
commit b10ad662b0
14 changed files with 1711 additions and 2 deletions

View file

@ -12,6 +12,7 @@
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering.Walk;
namespace AcDream.App.Rendering;
@ -114,6 +115,21 @@ public sealed class LoadedCell
/// test fixtures use low ids for interior cells, so keying on the id would misfire.
/// </summary>
public bool IsOutdoorNode;
/// <summary>
/// Campaign FW3.1: the retail frame walk's model of this cell (portal
/// side/exact-match decode, portal polygons + planes, stab list,
/// UNLIFTED transform), built atomically alongside this
/// <see cref="LoadedCell"/> by
/// <c>EnvCellLandblockBuildBuilder.BuildVisibilityCell</c> — see
/// <c>WalkCellFactory.FromParsed</c>. Null only for hand-built test
/// fixtures that construct a <see cref="LoadedCell"/> directly instead of
/// through the streaming build. <c>WalkProductionFrameContext.GetVisible</c>
/// is the walk's sole read of this field — the committed registry
/// (<see cref="CellVisibility.TryGetCell"/>) is the walk's only cell
/// source; its dead BFS role is unrelated.
/// </summary>
public WalkCell? Walk { get; internal set; }
}
/// <summary>

View file

@ -0,0 +1,191 @@
using System.Numerics;
using AcDream.Content;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
using DatReaderWriter.Types;
namespace AcDream.App.Rendering.Walk;
/// <summary>
/// Campaign FW3.1 — production <see cref="WalkBuilding"/> construction,
/// ported from the FW1 conformance harness
/// (tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapter.cs::BuildBuildings
/// /ConvertDrawingBsp/BuildGfxPolygon) to the legal
/// <see cref="IDatReaderWriter"/> seam. This closes the FW3.1 gap: today's
/// <c>Wb.BuildingLoader.AddBuilding</c> (update thread) reads
/// <c>BuildingInfo.Portals</c> only for BFS seeding and drops ModelId,
/// Frame, portal Flags/StabList, and the drawing BSP the frame walk needs —
/// this factory reads the SAME <c>BuildingInfo</c> plus the GfxObj/
/// GfxObjDegradeInfo chain, on the worker thread inside
/// <c>LandblockBuildFactory</c> where <see cref="IDatReaderWriter"/> access
/// is legal (<c>BuildingLoader</c> itself never touches DATs — it runs
/// update-thread, after the streaming build already completed).
///
/// Adjudication toggles the FW1 harness carried as public mutable fields for
/// sweep testing are PINNED here to their winning arms (2026-08-30 data-pin,
/// verified against the ten oracle traces):
/// <list type="bullet">
/// <item>GfxObj portal-polygon planes are NOT flipped (the plain
/// first-three-vertices cross survives every building-portal fixture).</item>
/// <item>Building portal_side is the INVERSE of the cell decode's 0x2 bit
/// (<c>WalkWorldDatAdapter.BuildingSideMode == 1</c>'s winning arm).</item>
/// </list>
/// </summary>
public static class WalkBuildingFactory
{
/// <summary>One built <see cref="WalkBuilding"/> plus its landblock-local
/// placement transform (<paramref name="WorldTransform"/> already has the
/// caller's block offset baked in — see <see cref="Build"/>).</summary>
public sealed record Entry(
WalkBuilding Building, Matrix4x4 WorldTransform, Matrix4x4 InverseWorldTransform);
/// <summary>Builds every building of one landblock. <paramref name="lbOffset"/>
/// is the SAME landblock-local world offset every other production
/// builder in this streaming job uses (<c>LandblockBuildFactory</c>'s
/// <c>lbOffset</c>/<c>worldOffset</c> convention) — baked directly into
/// each building's world transform, which is mathematically identical to
/// the FW1 harness's two-step compose (build landblock-local, then
/// post-multiply the block offset translation): translations commute,
/// so <c>R·T(origin)·T(lbOffset) == R·T(origin+lbOffset)</c>.</summary>
public static List<Entry> Build(
IDatReaderWriter dats, uint landblockId,
IReadOnlyList<BuildingInfo>? buildingInfos, Vector3 lbOffset)
{
var result = new List<Entry>();
if (buildingInfos is null)
return result;
uint lbMask = landblockId & 0xFFFF0000u;
foreach (BuildingInfo buildingInfo in buildingInfos)
{
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;
Vector3 sortCenter = Vector3.Zero;
var degradeLevels = new List<WalkBuildingDegradeLevel>();
if (dats.Get<GfxObj>(buildingInfo.ModelId) is GfxObj gfxObj)
{
bsp = ConvertDrawingBsp(gfxObj, gfxObj.DrawingBSP?.Root);
sortCenter = new Vector3(gfxObj.SortCenter.X, gfxObj.SortCenter.Y, gfxObj.SortCenter.Z);
if (gfxObj.DIDDegrade != 0
&& dats.Get<GfxObjDegradeInfo>(gfxObj.DIDDegrade)
is GfxObjDegradeInfo degradeInfo)
{
foreach (GfxObjInfo level in degradeInfo.Degrades)
{
WalkBspNode? levelBsp = null;
if (level.Id != 0
&& dats.Get<GfxObj>((uint)level.Id) is GfxObj levelGfx)
{
levelBsp = ConvertDrawingBsp(levelGfx, levelGfx.DrawingBSP?.Root);
}
degradeLevels.Add(
new WalkBuildingDegradeLevel(level.MinDist, level.IdealDist, level.MaxDist, levelBsp));
}
}
}
Matrix4x4 worldTransform =
Matrix4x4.CreateFromQuaternion(buildingInfo.Frame.Orientation)
* Matrix4x4.CreateTranslation(origin + lbOffset);
Matrix4x4.Invert(worldTransform, out Matrix4x4 inverse);
result.Add(new Entry(
new WalkBuilding
{
PositionCellId = positionCellId,
Portals = portals,
DrawingBsp = bsp,
DegradeLevels = degradeLevels.ToArray(),
SortCenter = sortCenter,
},
worldTransform,
inverse));
}
return result;
}
private static int DecodeBuildingSide(ushort flags) => (flags & 0x2) != 0 ? 0 : 1;
private 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 == BSPNodeType.Leaf,
PosNode = ConvertDrawingBsp(gfxObj, node.PosNode),
NegNode = ConvertDrawingBsp(gfxObj, node.NegNode),
};
if (node.Type == 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? 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])),
};
}
}

View file

@ -0,0 +1,75 @@
using System.Diagnostics.CodeAnalysis;
namespace AcDream.App.Rendering.Walk;
/// <summary>
/// Campaign FW3.1 — the walk's per-landblock <see cref="WalkBuilding"/>
/// registry: the production sibling of
/// <see cref="AcDream.App.Rendering.Wb.BuildingRegistry"/>. Publish/retire
/// mirror that registry's landblock lifecycle exactly — both are committed
/// together in <c>LandblockRenderPublisher.AdvanceCompleteOne</c>'s
/// <c>BuildingRegistryCommitted</c> step and retired together from
/// <c>LandblockRenderPublisher.RemoveBuildingRegistry</c> — because a walk
/// building and its BFS-derived <c>Wb.Building</c> counterpart come from the
/// SAME <c>BuildingInfo</c> array at the SAME landblock commit.
///
/// The reverse index (<see cref="TryGetEntry"/>) exists because
/// <see cref="WalkProductionFrameContext"/> resolves a building's placement
/// by REFERENCE during the portal pass — <c>ViewpointInBuilding</c>,
/// <c>ViewerDistanceTo</c>, and <c>ClipBuildingPolygon</c> are called many
/// times per building per frame once FW3.2 wires the walk into the render
/// loop, so this must be O(1), not a per-landblock scan.
/// </summary>
public sealed class WalkBuildingRegistry
{
private readonly Dictionary<uint, IReadOnlyList<WalkBuildingFactory.Entry>> _byLandblock = new();
private readonly Dictionary<WalkBuilding, WalkBuildingFactory.Entry> _byBuilding = new();
/// <summary>Atomically replaces one landblock's complete building set.
/// Mirrors <c>Wb.BuildingRegistry</c>'s "no partial landblock" commit
/// discipline — <paramref name="entries"/> is the immutable, fully-built
/// list carried by the streaming worker's
/// <c>Wb.EnvCellLandblockBuild.WalkBuildings</c>.</summary>
public void Publish(uint landblockId, IReadOnlyList<WalkBuildingFactory.Entry> entries)
{
uint key = landblockId & 0xFFFF0000u;
if (_byLandblock.TryGetValue(key, out IReadOnlyList<WalkBuildingFactory.Entry>? previous))
{
foreach (WalkBuildingFactory.Entry entry in previous)
_byBuilding.Remove(entry.Building);
}
_byLandblock[key] = entries;
foreach (WalkBuildingFactory.Entry entry in entries)
_byBuilding[entry.Building] = entry;
}
/// <summary>Removes every building of one landblock. Safe to call on a
/// landblock that never published (no-op).</summary>
public void Retire(uint landblockId)
{
uint key = landblockId & 0xFFFF0000u;
if (_byLandblock.Remove(key, out IReadOnlyList<WalkBuildingFactory.Entry>? previous))
{
foreach (WalkBuildingFactory.Entry entry in previous)
_byBuilding.Remove(entry.Building);
}
}
/// <summary>The buildings of one landblock, or empty when none are
/// published (unloaded, far-tier, or no <c>BuildingInfo</c> entries).</summary>
public IReadOnlyList<WalkBuildingFactory.Entry> GetBuildings(uint landblockId) =>
_byLandblock.TryGetValue(landblockId & 0xFFFF0000u, out IReadOnlyList<WalkBuildingFactory.Entry>? list)
? list
: Array.Empty<WalkBuildingFactory.Entry>();
/// <summary>O(1) reverse lookup: this building's committed placement.
/// False means the building is not (or no longer) committed — the walk
/// must treat that as a hard desync, never a silent skip (the FW
/// fail-loud rule).</summary>
public bool TryGetEntry(
WalkBuilding building, [MaybeNullWhen(false)] out WalkBuildingFactory.Entry entry) =>
_byBuilding.TryGetValue(building, out entry);
/// <summary>Number of landblocks with committed buildings (diagnostics).</summary>
public int LandblockCount => _byLandblock.Count;
}

View file

@ -0,0 +1,167 @@
using System.Numerics;
using AcDream.Content;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Types;
using Environment = DatReaderWriter.DBObjs.Environment;
namespace AcDream.App.Rendering.Walk;
/// <summary>
/// Campaign FW3.1 — production <see cref="WalkCell"/> construction, ported
/// from the FW1 conformance harness
/// (tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapter.cs::BuildCell)
/// to the legal <see cref="IDatReaderWriter"/> seam
/// (<c>RuntimeDatAccessArchitectureTests</c> forbids raw
/// <c>DatCollection</c> anywhere in this assembly outside
/// <c>RuntimeDatCollectionFactory</c>/<c>DatCollectionAdapter</c>).
///
/// <see cref="FromParsed"/> is the shared core: it takes an ALREADY-FETCHED
/// <see cref="EnvCell"/>/<see cref="CellStruct"/> pair and the production
/// UNLIFTED cell transform. <c>EnvCellLandblockBuildBuilder.BuildVisibilityCell</c>
/// (src/AcDream.App/Rendering/Wb/EnvCellLandblockBuild.cs) calls this
/// directly — it already has both parsed and the transform in hand from its
/// own <see cref="LoadedCell"/> construction, so no extra DAT read is spent.
/// <see cref="BuildCell"/> is the standalone DAT-reading convenience for
/// callers that only have a cell id (the walk landscape's stab-list
/// look-in cells, and the FW3 conformance harness).
///
/// Conventions (fixture-pinned; the ten oracle traces are the referee — see
/// <c>docs/research/2026-08-30-fw-walk-oracle/</c>):
/// <list type="bullet">
/// <item>portal_side = <c>(Flags &amp; 0x2) != 0 ? 0 : 1</c> — the INVERSE of
/// the bit, matching <c>EnvCellLandblockBuildBuilder.BuildVisibilityCell</c>'s
/// existing <c>PortalClipPlane.InsideSide</c> decode
/// (<c>(Flags &amp; 0x2) == 0 ? 1 : 0</c> — the same formula).</item>
/// <item>Exit-portal sentinel widening: dat <c>0xFFFF</c> →
/// <c>0xFFFFFFFF</c> (<see cref="WalkCellPortal.OtherCellId"/> is a full
/// cell id; the dat field is a 16-bit local id).</item>
/// <item>Portal-polygon plane: first-three-vertices cross,
/// <c>d = dot(N, p0)</c> — the same formula
/// <c>BuildVisibilityCell</c>'s <c>PortalClipPlane</c> already uses.</item>
/// </list>
/// </summary>
public static class WalkCellFactory
{
/// <summary>DAT-reading entry point: fetches EnvCell/Environment/CellStruct
/// and builds their landblock-local (<paramref name="blockOffset"/>
/// added) UNLIFTED transform before delegating to <see cref="FromParsed"/>.
/// Returns null when the cell, its environment, or its cell structure
/// cannot be resolved (unregistered/degenerate cell — same as the FW1
/// test adapter).</summary>
public static WalkCell? BuildCell(IDatReaderWriter 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;
}
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 FromParsed(cellId, envCell, cellStruct, worldTransform, inverse);
}
/// <summary>Builds a <see cref="WalkCell"/> from an already-fetched
/// EnvCell/CellStruct pair and the production cell transform. No DAT
/// access — safe to call from a context that already holds the reader
/// lock for an unrelated read.</summary>
public static WalkCell FromParsed(
uint cellId, EnvCell envCell, CellStruct cellStruct,
Matrix4x4 worldTransform, Matrix4x4 inverseWorldTransform)
{
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 ? 0 : 1,
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(),
WorldTransform = worldTransform,
InverseWorldTransform = inverseWorldTransform,
};
}
/// <summary>Every interior cell of one landblock, bounded by
/// <c>LandBlockInfo.NumCells</c> (the production bound —
/// <c>LandblockBuildFactory.BuildInteriorEntitiesForStreaming</c> uses
/// the same count; the FW1 test adapter instead scans until the first
/// miss, which is equivalent for well-formed installed DATs).</summary>
public static Dictionary<uint, WalkCell> BuildInteriorCells(
IDatReaderWriter dats, uint landblockId, Vector3 blockOffset,
Dictionary<uint, WalkCell>? into = null)
{
uint lbMask = landblockId & 0xFFFF0000u;
Dictionary<uint, WalkCell> cells = into ?? new Dictionary<uint, WalkCell>();
if (dats.Get<LandBlockInfo>(lbMask | 0xFFFEu) is not LandBlockInfo info)
return cells;
uint firstCellId = lbMask | 0x0100u;
for (uint offset = 0; offset < info.NumCells; offset++)
{
WalkCell? cell = BuildCell(dats, firstCellId + offset, blockOffset);
if (cell is not null)
cells[cell.CellId] = cell;
}
return cells;
}
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? 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])),
};
}
}

View file

@ -0,0 +1,191 @@
using System.Numerics;
namespace AcDream.App.Rendering.Walk;
/// <summary>
/// Campaign FW3.1 — the production sibling of the FW1 conformance harness's
/// <c>WalkLandscapeDatBuilder</c>
/// (tests/AcDream.App.Tests/Rendering/Walk/WalkLandscapeDatBuilder.cs): owns
/// retail's viewer-centred block grid (LScape <c>mid_radius</c> = 25 → a
/// 51×51 window, recon 2026-08-30) and its LOD ring pyramid
/// (<see cref="SideCellCountForRing"/>).
///
/// Unlike the test builder — which re-scans the whole grid from a
/// <c>DatCollection</c> on every call — this class is fed INCREMENTALLY from
/// landblock commit/retire (<see cref="PublishLandblock"/>/
/// <see cref="RetireLandblock"/>), the same lifecycle
/// <c>Wb.BuildingRegistry</c> and <c>CellVisibility</c> already follow. Each
/// landblock's z-slab and building placements are computed ONCE, on the
/// streaming worker thread, at landblock-build time
/// (<c>LandblockBuildFactory</c>/<c>WalkBuildingFactory</c>) — publishing
/// here is a pure dictionary write, no DAT or GfxObj work.
///
/// <see cref="SetViewer"/> recentres the grid on the camera's block, porting
/// the test builder's viewer-cell recompute; it goes further by also
/// reflowing the 51×51 window when the camera crosses into a different
/// landblock (the test builder never needs this — every moving fixture stays
/// within its anchor block). Reflow only touches already-published data (a
/// dictionary lookup per slot); no DAT access ever happens here.
///
/// No frame currently calls <see cref="SetViewer"/> or reads
/// <see cref="Landscape"/> — FW3.2 wires <c>RetailFrameWalk</c> into the
/// render loop. This class exists now so that wiring is additive, and so the
/// FW3.1 conformance gate can exercise the SAME assembly code path
/// production will drive.
/// </summary>
public sealed class WalkLandscapeAssembler
{
/// <summary>Retail LScape <c>mid_radius</c> (recon 2026-08-30) — NOT
/// acdream's streaming radius (two-tier N1/N2), which is smaller. Blocks
/// outside the streamed window simply stay unpublished (null slots).</summary>
public const int MidRadius = 25;
public const int GridWidth = MidRadius * 2 + 1;
private sealed class BlockData
{
public required float MaxZ;
public required float MinZ;
public required IReadOnlyList<WalkBuildingFactory.Entry> Buildings;
}
private readonly Dictionary<(int X, int Y), BlockData> _blocks = new();
private int _viewerBlockX = int.MinValue;
private int _viewerBlockY = int.MinValue;
public WalkLandscape Landscape { get; } = new()
{
MidWidth = GridWidth,
Blocks = new WalkLandBlock?[GridWidth * GridWidth],
ViewerBlockX = MidRadius,
ViewerBlockY = MidRadius,
};
/// <summary>Landblock commit
/// (<c>LandblockRenderPublisher.AdvanceCompleteOne</c>'s
/// <c>BuildingRegistryCommitted</c> step): register/replace this
/// landblock's z-slab and building placements. If the landblock falls
/// within the CURRENT window, the visible slot refreshes immediately.</summary>
public void PublishLandblock(
uint landblockId, float maxZ, float minZ,
IReadOnlyList<WalkBuildingFactory.Entry> buildings)
{
(int bx, int by) = BlockCoords(landblockId);
_blocks[(bx, by)] = new BlockData { MaxZ = maxZ, MinZ = minZ, Buildings = buildings };
RefreshSlotIfWindowed(bx, by);
}
/// <summary>Landblock retirement (<c>LandblockRenderPublisher
/// .RemoveBuildingRegistry</c>): drop this landblock's data. Safe to call
/// on a landblock that never published (no-op).</summary>
public void RetireLandblock(uint landblockId)
{
(int bx, int by) = BlockCoords(landblockId);
_blocks.Remove((bx, by));
RefreshSlotIfWindowed(bx, by);
}
/// <summary>Recentres the grid on the camera's block and updates the
/// sub-block cell offsets (retail's viewer recentre —
/// <c>WalkLandscapeDatBuilder.SetViewer</c>'s port). When the camera's
/// block hasn't changed since the last call, this only updates
/// <see cref="WalkLandscape.ViewerCellX"/>/<see cref="WalkLandscape.ViewerCellY"/>
/// — zero allocation, matching the test builder's lightweight path. A
/// block crossing additionally reflows the 51×51 window from already-
/// published data (a bounded, allocation-free dictionary-lookup pass —
/// no DAT/GfxObj work, which already happened at publish time).</summary>
public void SetViewer(uint cameraCellId, Vector3 cameraOrigin)
{
int cameraBlockX = (int)(cameraCellId >> 24);
int cameraBlockY = (int)((cameraCellId >> 16) & 0xFF);
if (cameraBlockX != _viewerBlockX || cameraBlockY != _viewerBlockY)
{
_viewerBlockX = cameraBlockX;
_viewerBlockY = cameraBlockY;
RebuildWindow();
}
uint low = cameraCellId & 0xFFFFu;
if (low >= 1 && low <= 0x40)
{
int cellIndex = (int)low - 1;
Landscape.ViewerCellX = cellIndex / 8;
Landscape.ViewerCellY = cellIndex % 8;
}
else
{
// Interior camera: derive the outside-projected landcell from
// the camera origin, matching the test builder's fallback
// (Position::get_outside_cell_id via SmartBox::RenderNormalMode's
// seen_outside arm).
Landscape.ViewerCellX = Math.Clamp((int)MathF.Floor(cameraOrigin.X / 24f), 0, 7);
Landscape.ViewerCellY = Math.Clamp((int)MathF.Floor(cameraOrigin.Y / 24f), 0, 7);
}
}
/// <summary><c>ring &lt;= 1 ? 8 : ring == 2 ? 4 : ring &lt;= 4 ? 2 : 1</c>
/// — the live-observed resolution pyramid (recon 2026-08-30): 8×8 in the
/// 3×3 core, 4×4 at ring 2, 2×2 at rings 34, 1×1 beyond. Buildings only
/// attach at full resolution (ring ≤ 1) — the traces show no BLD beyond
/// ±1 block.</summary>
internal static int SideCellCountForRing(int ring)
=> ring <= 1 ? 8 : ring == 2 ? 4 : ring <= 4 ? 2 : 1;
internal static int RingOf(int gridX, int gridY)
=> Math.Max(Math.Abs(gridX - MidRadius), Math.Abs(gridY - MidRadius));
private void RebuildWindow()
{
for (int gx = 0; gx < GridWidth; gx++)
{
for (int gy = 0; gy < GridWidth; gy++)
{
int bx = _viewerBlockX + gx - MidRadius;
int by = _viewerBlockY + gy - MidRadius;
Landscape.Blocks[gx * GridWidth + gy] = BuildSlot(bx, by, gx, gy);
}
}
}
private void RefreshSlotIfWindowed(int bx, int by)
{
if (_viewerBlockX == int.MinValue)
return; // SetViewer has never run — no window to refresh yet.
int gx = bx - _viewerBlockX + MidRadius;
int gy = by - _viewerBlockY + MidRadius;
if (gx < 0 || gx >= GridWidth || gy < 0 || gy >= GridWidth)
return;
Landscape.Blocks[gx * GridWidth + gy] = BuildSlot(bx, by, gx, gy);
}
private WalkLandBlock? BuildSlot(int bx, int by, int gx, int gy)
{
if (bx < 0 || bx > 0xFF || by < 0 || by > 0xFF)
return null;
if (!_blocks.TryGetValue((bx, by), out BlockData? data))
return null;
int sideCellCount = SideCellCountForRing(RingOf(gx, gy));
var block = new WalkLandBlock
{
SideCellCount = sideCellCount,
MaxZ = data.MaxZ,
MinZ = data.MinZ,
};
block.EnsureCellArrays();
if (sideCellCount == 8)
{
foreach (WalkBuildingFactory.Entry entry in data.Buildings)
{
int cellIndex = (int)(entry.Building.PositionCellId & 0xFFFFu) - 1;
if (cellIndex >= 0 && cellIndex < 64)
block.CellBuildings[cellIndex] = entry.Building;
}
}
return block;
}
private static (int X, int Y) BlockCoords(uint landblockId)
=> ((int)((landblockId >> 24) & 0xFFu), (int)((landblockId >> 16) & 0xFFu));
}

View file

@ -0,0 +1,174 @@
using System.Numerics;
namespace AcDream.App.Rendering.Walk;
/// <summary>
/// Campaign FW3.1 — the production <see cref="IWalkFrameContext"/> /
/// <see cref="IRetailFrameWalkContext"/> / <see cref="IWalkBuildingFrameContext"/>
/// implementation: cells resolve through the committed
/// <see cref="CellVisibility"/> registry (<c>LoadedCell.Walk</c>, per the FW
/// binding rule that the walk consumes ONLY the committed registry —
/// <c>CellVisibility.TryGetCell</c>, never a synthetic/dead-code path);
/// buildings resolve through <see cref="WalkBuildingRegistry"/>; the camera
/// pose, projection, and viewport are supplied by the caller.
///
/// Deliberately NOT coupled to any concrete camera type — FW3.2 will supply
/// live values from <c>WorldCameraFrame</c>; wiring that in is additive.
/// The ray caster is a GENERIC inverse-view-projection unprojection, not the
/// capture client's exact <c>Render::xinvscale</c>/<c>tx</c>/<c>vdst</c>
/// constants the FW1 conformance harness's <c>WalkTraceReplayContext</c>
/// uses — those are FIXTURE PINS specific to the 1024×720 capture client,
/// not production values. This is safe because
/// <see cref="WalkCopyView.Append"/> only ever CROSS-PRODUCTS these rays to
/// build view-edge planes: a uniform scale or additive offset along a ray
/// cancels out of every cross product it feeds, so any two points along the
/// true eye ray (near/far unprojection) are observably equivalent to
/// retail's exact construction for this contract.
///
/// One instance is a per-frame value (like <c>WalkTraceReplayContext</c>):
/// construct fresh each frame with that frame's camera pose.
/// </summary>
public sealed class WalkProductionFrameContext : IWalkFrameContext, IRetailFrameWalkContext
{
/// <summary><c>Render::znear</c> @0x0081ec84 / <c>set_vdst</c> @0x0054b240.</summary>
public const float ZNear = 0.1f;
private sealed class InverseViewProjectionRayCaster : IWalkRayCaster
{
private readonly Matrix4x4 _inverseViewProjection;
private readonly float _viewportWidth;
private readonly float _viewportHeight;
public InverseViewProjectionRayCaster(
Matrix4x4 viewProjection, float viewportWidth, float viewportHeight)
{
if (!Matrix4x4.Invert(viewProjection, out _inverseViewProjection))
{
throw new ArgumentException(
"The walk's view-projection matrix must be invertible.",
nameof(viewProjection));
}
_viewportWidth = viewportWidth;
_viewportHeight = viewportHeight;
}
/// <summary>Screen space: origin top-left, +Y down — matching
/// <see cref="WalkScreenClip.TransformToScreen"/> and
/// <see cref="WalkCopyView.AppendFullViewportQuad"/>'s quad
/// winding.</summary>
public Vector3 RayThrough(float screenX, float screenY)
{
float ndcX = screenX / _viewportWidth * 2f - 1f;
float ndcY = 1f - screenY / _viewportHeight * 2f;
Vector4 near = Vector4.Transform(
new Vector4(ndcX, ndcY, 0f, 1f), _inverseViewProjection);
Vector4 far = Vector4.Transform(
new Vector4(ndcX, ndcY, 1f, 1f), _inverseViewProjection);
Vector3 nearWorld = new Vector3(near.X, near.Y, near.Z) / near.W;
Vector3 farWorld = new Vector3(far.X, far.Y, far.Z) / far.W;
return farWorld - nearWorld;
}
}
private readonly CellVisibility _cells;
private readonly WalkBuildingRegistry _buildings;
private readonly Matrix4x4 _viewProjection;
private readonly IWalkRayCaster _rays;
private Vector2[] _activeViewVerts = new Vector2[32];
private int _activeViewVertCount;
public WalkProductionFrameContext(
CellVisibility cells,
WalkBuildingRegistry buildings,
Vector3 worldViewpoint,
Vector3 forward,
Matrix4x4 viewProjection,
float viewportWidth,
float viewportHeight)
{
_cells = cells ?? throw new ArgumentNullException(nameof(cells));
_buildings = buildings ?? throw new ArgumentNullException(nameof(buildings));
WorldViewpoint = worldViewpoint;
_viewProjection = viewProjection;
ViewportWidth = viewportWidth;
ViewportHeight = viewportHeight;
_rays = new InverseViewProjectionRayCaster(viewProjection, viewportWidth, viewportHeight);
// The retail CY near plane: N = forward, d = -dot(eye, forward) - znear.
CyPlane = new WalkPlane(forward, -Vector3.Dot(worldViewpoint, forward) - ZNear);
}
public Vector3 WorldViewpoint { get; }
public float ViewportWidth { get; }
public float ViewportHeight { get; }
public WalkPlane CyPlane { get; }
public IWalkRayCaster Rays => _rays;
public IWalkFrameContext CellContext => this;
public Vector3 ViewpointIn(WalkCell cell)
=> Vector3.Transform(WorldViewpoint, cell.InverseWorldTransform);
public Matrix4x4 ObjectToClip(WalkCell cell) => cell.WorldTransform * _viewProjection;
/// <summary>Resolves through the committed <see cref="CellVisibility"/>
/// registry only — a missing/uncommitted cell returns null (retail's
/// portal-skip behavior; the FW binding rule requires any such miss to
/// be diagnostically counted under a flag rather than silently swallowed
/// once a caller wires one up — this seam does not itself log, matching
/// <see cref="IWalkFrameContext.GetVisible"/>'s documented contract).</summary>
public WalkCell? GetVisible(uint cellId)
=> _cells.TryGetCell(cellId, out LoadedCell? cell) ? cell?.Walk : null;
public void SetActiveView(WalkPortalView views, int index)
{
WalkViewPoly poly = views.View.Polys[index];
if (_activeViewVerts.Length < poly.VertexCount)
_activeViewVerts = new Vector2[poly.VertexCount];
for (int k = 0; k < poly.VertexCount; k++)
_activeViewVerts[k] = views.View.Vertices[poly.VertexIndex + k].Point;
_activeViewVertCount = poly.VertexCount;
}
public Vector3 ViewpointInBuilding(WalkBuilding building)
=> Vector3.Transform(WorldViewpoint, GetEntry(building).InverseWorldTransform);
/// <summary><c>CPhysicsPart::UpdateViewerDistance</c> @0x0050e030: the
/// distance to the part's SCALED sort center, not the position origin.</summary>
public float ViewerDistanceTo(WalkBuilding building)
{
WalkBuildingFactory.Entry entry = GetEntry(building);
return Vector3.Distance(
WorldViewpoint, Vector3.Transform(building.SortCenter, entry.WorldTransform));
}
public int ClipBuildingPolygon(
WalkBuilding building, WalkPolygon polygon, int side, Span<WalkScreenPoint> output)
{
Matrix4x4 objectToClip = GetEntry(building).WorldTransform * _viewProjection;
Span<WalkScreenPoint> projected = stackalloc WalkScreenPoint[polygon.Vertices.Length];
for (int i = 0; i < polygon.Vertices.Length; i++)
{
projected[i] = WalkScreenClip.TransformToScreen(
polygon.Vertices[i], objectToClip, ViewportWidth, ViewportHeight);
}
if (side != 0)
projected.Reverse();
return WalkScreenClip.ClipAgainstView(
projected, _activeViewVerts.AsSpan(0, _activeViewVertCount), output);
}
private WalkBuildingFactory.Entry GetEntry(WalkBuilding building)
{
if (!_buildings.TryGetEntry(building, out var entry))
{
// Fail loud (the PV3 post-mortem rule): a building the walk is
// actively placing MUST be committed in the same registry the
// walk was handed — a miss here is a walk/registry desync, never
// a silently-skipped building.
throw new InvalidOperationException(
"WalkProductionFrameContext was asked to place a WalkBuilding " +
"that is not committed in its WalkBuildingRegistry.");
}
return entry;
}
}

View file

@ -1,5 +1,6 @@
using System.Collections.Immutable;
using System.Numerics;
using AcDream.App.Rendering.Walk;
using AcDream.Core.Rendering.Wb;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
@ -34,22 +35,48 @@ public sealed class EnvCellLandblockBuild
public EnvCellLandblockBuild(
uint landblockId,
IEnumerable<LoadedCell> visibilityCells,
IEnumerable<EnvCellShellPlacement> shells)
IEnumerable<EnvCellShellPlacement> shells,
IEnumerable<WalkBuildingFactory.Entry>? walkBuildings = null,
float walkMaxZ = 0f,
float walkMinZ = 0f)
{
LandblockId = landblockId;
VisibilityCells = visibilityCells.ToImmutableArray();
Shells = shells.ToImmutableArray();
WalkBuildings = (walkBuildings ?? Enumerable.Empty<WalkBuildingFactory.Entry>()).ToImmutableArray();
WalkMaxZ = walkMaxZ;
WalkMinZ = walkMinZ;
uint expectedPrefix = landblockId & 0xFFFF0000u;
if (VisibilityCells.Any(cell => (cell.CellId & 0xFFFF0000u) != expectedPrefix))
throw new ArgumentException("A visibility cell belongs to a different landblock.", nameof(visibilityCells));
if (Shells.Any(shell => (shell.CellId & 0xFFFF0000u) != expectedPrefix))
throw new ArgumentException("A render shell belongs to a different landblock.", nameof(shells));
if (WalkBuildings.Any(entry => (entry.Building.PositionCellId & 0xFFFF0000u) != expectedPrefix))
throw new ArgumentException("A walk building belongs to a different landblock.", nameof(walkBuildings));
}
public uint LandblockId { get; }
public ImmutableArray<LoadedCell> VisibilityCells { get; }
public ImmutableArray<EnvCellShellPlacement> Shells { get; }
/// <summary>Campaign FW3.1: this landblock's walk building placements
/// (<c>WalkBuildingFactory.Build</c>, computed worker-side alongside
/// <see cref="VisibilityCells"/>). Committed atomically with the rest of
/// this transaction — see <c>WalkBuildingRegistry.Publish</c> at
/// <c>LandblockRenderPublisher.AdvanceCompleteOne</c>.</summary>
public ImmutableArray<WalkBuildingFactory.Entry> WalkBuildings { get; }
/// <summary>Campaign FW3.1: this landblock's retail z-slab
/// (<c>heightTable[maxByte] + 200</c>) — the walk landscape's per-block
/// visibility bound (<c>WalkLandscapeAssembler.PublishLandblock</c>).
/// Computed once, worker-side, from the SAME heightmap bytes + height
/// table the terrain mesh build already reads — no extra DAT access.</summary>
public float WalkMaxZ { get; }
/// <summary>Campaign FW3.1: this landblock's retail z-slab
/// (<c>heightTable[minByte] 1</c>). See <see cref="WalkMaxZ"/>.</summary>
public float WalkMinZ { get; }
}
/// <summary>
@ -62,6 +89,9 @@ public sealed class EnvCellLandblockBuildBuilder
private readonly uint _landblockId;
private readonly List<LoadedCell> _visibilityCells = new();
private readonly List<EnvCellShellPlacement> _shells = new();
private readonly List<WalkBuildingFactory.Entry> _walkBuildings = new();
private float _walkMaxZ;
private float _walkMinZ;
private bool _built;
public EnvCellLandblockBuildBuilder(uint landblockId)
@ -69,6 +99,31 @@ public sealed class EnvCellLandblockBuildBuilder
_landblockId = landblockId;
}
/// <summary>Campaign FW3.1: registers this landblock's walk building
/// placements (<c>WalkBuildingFactory.Build</c>'s output). Additive to
/// the existing cell/shell accumulation — called once per landblock from
/// <c>LandblockBuildFactory.BuildInteriorEntitiesForStreaming</c>, where
/// <c>LandBlockInfo</c> is already in hand.</summary>
public void AddWalkBuildings(IEnumerable<WalkBuildingFactory.Entry> entries)
{
if (_built)
throw new InvalidOperationException("This landblock cell build is already complete.");
_walkBuildings.AddRange(entries);
}
/// <summary>Campaign FW3.1: registers this landblock's retail z-slab
/// (<see cref="EnvCellLandblockBuild.WalkMaxZ"/>/<see cref="EnvCellLandblockBuild.WalkMinZ"/>).
/// Independent of interior-cell/building presence — every near-tier
/// landblock has outdoor terrain heights, so
/// <c>LandblockBuildFactory.BuildLocked</c> calls this unconditionally.</summary>
public void SetWalkZSlab(float maxZ, float minZ)
{
if (_built)
throw new InvalidOperationException("This landblock cell build is already complete.");
_walkMaxZ = maxZ;
_walkMinZ = minZ;
}
public void AddCell(
uint envCellId,
EnvCell envCell,
@ -116,7 +171,8 @@ public sealed class EnvCellLandblockBuildBuilder
if (_built)
throw new InvalidOperationException("This landblock cell build is already complete.");
_built = true;
return new EnvCellLandblockBuild(_landblockId, _visibilityCells, _shells);
return new EnvCellLandblockBuild(
_landblockId, _visibilityCells, _shells, _walkBuildings, _walkMaxZ, _walkMinZ);
}
/// <summary>
@ -290,6 +346,20 @@ public sealed class EnvCellLandblockBuildBuilder
PortalPolygons = portalPolygons,
VisibleCells = visibleCells,
SeenOutside = envCell.Flags.HasFlag(EnvCellFlags.SeenOutside),
// Campaign FW3.1: the walk's model of this cell, built from the
// SAME EnvCell/CellStruct/transform this LoadedCell was just
// built from — WalkCellFactory.FromParsed does its own portal
// pass (independent of the clipPlanes/portalPolygons above; the
// two consumers decode the same dat fields for different needs)
// so it stays a direct, auditable port of the FW1 test adapter
// rather than reshaping LoadedCell's own list layout around it.
// cellTransform/inverse here are UNLIFTED (this method's
// cellOrigin/cellTransform params are the caller's
// physicsCellOrigin/physicsCellTransform — see
// LandblockBuildFactory.BuildInteriorEntitiesForStreaming, which
// keeps the +0.02 m ShellDrawLiftZ out of this transform) —
// exactly what the walk wants.
Walk = WalkCellFactory.FromParsed(envCellId, envCell, cellStruct, cellTransform, inverse),
};
}
}

View file

@ -178,6 +178,15 @@ public sealed class LandblockBuildFactory
worldOffset,
_heightTable));
var envCellBuild = new AcDream.App.Rendering.Wb.EnvCellLandblockBuildBuilder(landblockId);
// Campaign FW3.1: the walk landscape's per-block z-slab
// (CLandBlock::calc_lighting-adjacent unpack @0x0052f1d0). Every
// near-tier landblock has outdoor terrain heights regardless of
// whether it has interior cells or buildings, so this is
// unconditional — unlike WalkBuildings (added below, inside
// BuildInteriorEntitiesForStreaming, where LandBlockInfo is already
// in hand).
(float walkMaxZ, float walkMinZ) = ComputeWalkZSlab(baseLoaded.Heightmap.Height);
envCellBuild.SetWalkZSlab(walkMaxZ, walkMinZ);
merged.AddRange(BuildInteriorEntitiesForStreaming(
landblockId,
lbX,
@ -448,6 +457,17 @@ public sealed class LandblockBuildFactory
(lbY - origin.CenterY) * 192f,
0f);
// Campaign FW3.1: the walk's building placements (portals + drawing
// BSP + degrade ladder + sort center), read from the SAME
// LandBlockInfo.Buildings this method already fetched above — no
// extra DAT read. This closes the gap where Wb.BuildingLoader
// .AddBuilding (update thread, no DAT access) keeps only
// ModelId/Frame/portal-flags/stab-lists for its own BFS seeding and
// drops everything the frame walk needs.
envCellBuild.AddWalkBuildings(
AcDream.App.Rendering.Walk.WalkBuildingFactory.Build(
_dats, landblockId, lbInfo.Buildings, lbOffset));
// Per-landblock id namespace — see AcDream.Core.World.InteriorEntityIdAllocator
// for the full bit layout + history. Distinct from scenery (0x80000000+) and
// landblock stabs (0xC0000000+, ids from LandblockLoader).
@ -649,6 +669,24 @@ public sealed class LandblockBuildFactory
}
/// <summary>Campaign FW3.1: retail's per-block z-slab
/// (<c>WalkLandscapeDatBuilder</c>'s port target —
/// <c>CLandBlock::calc_lighting</c>-adjacent unpack @0x0052f1d0):
/// <c>max_zval = heightTable[maxByte] + 200</c>,
/// <c>min_zval = heightTable[minByte] - 1</c>, over the landblock's own
/// 81-byte heightmap. Uses <see cref="_heightTable"/> — already a
/// constructor field, so this needs no additional DAT read.</summary>
private (float MaxZ, float MinZ) ComputeWalkZSlab(byte[] heights)
{
byte maxByte = 0, minByte = 255;
foreach (byte h in heights)
{
if (h > maxByte) maxByte = h;
if (h < minByte) minByte = h;
}
return (_heightTable[maxByte] + 200f, _heightTable[minByte] - 1f);
}
private static float SampleTerrainZ(DatReaderWriter.DBObjs.LandBlock block, float[] heightTable, float localX, float localY)
{
uint landblockX = (block.Id >> 24) & 0xFFu;

View file

@ -3,6 +3,7 @@ using System.Diagnostics;
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Wb;
using AcDream.App.Rendering.Walk;
using AcDream.Core.Terrain;
namespace AcDream.App.Streaming;
@ -102,6 +103,17 @@ public sealed class LandblockRenderPublisher
private readonly Action<EnvCellLandblockBuild>? _prepareEnvCells;
private readonly Action<uint>? _removeEnvCells;
private readonly Dictionary<uint, BuildingRegistry> _buildingRegistries = new();
// Campaign FW3.1: the walk's production world-data siblings of
// _buildingRegistries. Owned here (not injected) exactly like
// _buildingRegistries — no caller constructs a LandblockRenderPublisher
// with pre-existing walk state. Committed/retired at the same points as
// the building registry (see AdvanceCompleteOne / RemoveBuildingRegistry
// below) because both come from the same landblock's BuildingInfo array
// at the same commit. No frame reads these yet (FW3.2 wires the walk
// into the render loop) — publication happens now so FW3.1's
// conformance gate exercises the production commit path.
private readonly WalkBuildingRegistry _walkBuildingRegistry = new();
private readonly WalkLandscapeAssembler _walkLandscape = new();
private long _beginCount;
private long _completeCount;
@ -148,6 +160,15 @@ public sealed class LandblockRenderPublisher
public IReadOnlyCollection<BuildingRegistry> BuildingRegistries =>
_buildingRegistries.Values;
/// <summary>Campaign FW3.1: the walk's production building placements,
/// keyed by landblock, committed alongside <see cref="BuildingRegistries"/>.</summary>
public WalkBuildingRegistry WalkBuildings => _walkBuildingRegistry;
/// <summary>Campaign FW3.1: the walk's production viewer-centred
/// landscape grid, fed from the same landblock commits as
/// <see cref="WalkBuildings"/>.</summary>
public WalkLandscapeAssembler WalkLandscape => _walkLandscape;
public LandblockRenderPublisherDiagnostics Diagnostics => new(
_beginCount,
_completeCount,
@ -339,6 +360,21 @@ public sealed class LandblockRenderPublisher
_buildingRegistries[registryKey] =
completedBuildings.Registry;
}
// Campaign FW3.1: publish this landblock's walk world data in
// the same step — build.EnvCells carries WalkBuildings/WalkMaxZ
// /WalkMinZ (computed worker-side by LandblockBuildFactory) for
// every near-tier build, even one with zero interior cells
// (the z-slab is unconditional; WalkBuildings is naturally empty
// when LandBlockInfo has none).
if (build.EnvCells is { } walkEnvCells)
{
_walkBuildingRegistry.Publish(landblockId, walkEnvCells.WalkBuildings);
_walkLandscape.PublishLandblock(
landblockId,
walkEnvCells.WalkMaxZ,
walkEnvCells.WalkMinZ,
walkEnvCells.WalkBuildings);
}
publication.BuildingRegistryCommitted = true;
}
else if (publication.EnvCellPublication is { } envCellPublication
@ -397,6 +433,14 @@ public sealed class LandblockRenderPublisher
public void RemoveBuildingRegistry(uint landblockId)
{
_buildingRegistries.Remove(landblockId & 0xFFFF0000u);
// Campaign FW3.1: retire the walk's siblings at the SAME retirement
// stage — they were committed together above, so they retire
// together (no new LandblockRetirementStage; folding into the
// existing BuildingRegistry stage keeps the retirement ticket state
// machine unchanged, which is the minimal/additive choice for a
// slice that does no frame wiring yet).
_walkBuildingRegistry.Retire(landblockId);
_walkLandscape.RetireLandblock(landblockId);
_buildingRegistryRemovalCount++;
}

View file

@ -0,0 +1,75 @@
using System.Numerics;
using AcDream.App.Rendering.Walk;
namespace AcDream.App.Tests.Rendering.Walk;
/// <summary>Campaign FW3.1 — hermetic (no DAT) coverage of the walk's
/// production building registry: landblock-keyed publish/retire and the
/// O(1) reverse index <see cref="WalkProductionFrameContext"/> relies on.</summary>
public sealed class WalkBuildingRegistryTests
{
private static WalkBuildingFactory.Entry Entry(uint positionCellId) =>
new(new WalkBuilding { PositionCellId = positionCellId }, Matrix4x4.Identity, Matrix4x4.Identity);
[Fact]
public void Publish_MakesBuildingsFindableByLandblockAndByReference()
{
var registry = new WalkBuildingRegistry();
WalkBuildingFactory.Entry entry = Entry(0xA9B40001u);
registry.Publish(0xA9B4FFFFu, new[] { entry });
Assert.Same(entry, Assert.Single(registry.GetBuildings(0xA9B40100u)));
Assert.True(registry.TryGetEntry(entry.Building, out var found));
Assert.Same(entry, found);
Assert.Equal(1, registry.LandblockCount);
}
[Fact]
public void Publish_ReplacesThePreviousLandblockAndDropsItsReverseIndexEntries()
{
var registry = new WalkBuildingRegistry();
WalkBuildingFactory.Entry first = Entry(0xA9B40001u);
WalkBuildingFactory.Entry second = Entry(0xA9B40002u);
registry.Publish(0xA9B4FFFFu, new[] { first });
registry.Publish(0xA9B4FFFFu, new[] { second });
Assert.Same(second, Assert.Single(registry.GetBuildings(0xA9B4FFFFu)));
Assert.False(registry.TryGetEntry(first.Building, out _));
Assert.True(registry.TryGetEntry(second.Building, out _));
Assert.Equal(1, registry.LandblockCount);
}
[Fact]
public void Retire_RemovesTheLandblockAndItsReverseIndexEntries()
{
var registry = new WalkBuildingRegistry();
WalkBuildingFactory.Entry entry = Entry(0xA9B40001u);
registry.Publish(0xA9B4FFFFu, new[] { entry });
registry.Retire(0xA9B40100u); // any id sharing the landblock prefix
Assert.Empty(registry.GetBuildings(0xA9B4FFFFu));
Assert.False(registry.TryGetEntry(entry.Building, out _));
Assert.Equal(0, registry.LandblockCount);
}
[Fact]
public void Retire_UnknownLandblockIsANoOp()
{
var registry = new WalkBuildingRegistry();
registry.Retire(0xA9B4FFFFu);
Assert.Equal(0, registry.LandblockCount);
}
[Fact]
public void GetBuildings_UnpublishedLandblockReturnsEmpty()
{
var registry = new WalkBuildingRegistry();
Assert.Empty(registry.GetBuildings(0xA9B4FFFFu));
}
}

View file

@ -0,0 +1,148 @@
using System.Numerics;
using AcDream.App.Rendering.Walk;
namespace AcDream.App.Tests.Rendering.Walk;
/// <summary>Campaign FW3.1 — hermetic (no DAT) coverage of the production
/// landscape assembler: publish/retire lifecycle, the LOD ring pyramid, and
/// the viewer-recentre grid reflow. The DAT-backed equivalence to the FW1
/// test builder is proven by <c>WalkProductionWorldConformanceTests</c>
/// (Lane=InstalledDat); this class covers the incremental publish/retire
/// machinery that harness doesn't exercise (it publishes and calls
/// SetViewer exactly once per fixture).</summary>
public sealed class WalkLandscapeAssemblerTests
{
private const uint LandblockId = 0xA9B4FFFFu; // block (0xA9, 0xB4)
private const uint CameraCellId = 0xA9B40001u; // same block, outdoor landcell 1
private static int GridIndex(int gx, int gy) => gx * WalkLandscapeAssembler.GridWidth + gy;
private static int CenterIndex() =>
GridIndex(WalkLandscapeAssembler.MidRadius, WalkLandscapeAssembler.MidRadius);
[Fact]
public void PublishBeforeSetViewer_IsVisibleOnceSetViewerRuns()
{
var assembler = new WalkLandscapeAssembler();
assembler.PublishLandblock(LandblockId, maxZ: 100f, minZ: -5f, Array.Empty<WalkBuildingFactory.Entry>());
assembler.SetViewer(CameraCellId, Vector3.Zero);
WalkLandBlock? block = assembler.Landscape.Blocks[CenterIndex()];
Assert.NotNull(block);
Assert.Equal(8, block!.SideCellCount);
Assert.Equal(100f, block.MaxZ);
Assert.Equal(-5f, block.MinZ);
}
[Fact]
public void PublishAfterSetViewer_RefreshesTheAlreadyWindowedSlotImmediately()
{
var assembler = new WalkLandscapeAssembler();
assembler.SetViewer(CameraCellId, Vector3.Zero);
assembler.PublishLandblock(LandblockId, maxZ: 42f, minZ: 3f, Array.Empty<WalkBuildingFactory.Entry>());
Assert.Equal(42f, assembler.Landscape.Blocks[CenterIndex()]!.MaxZ);
}
[Fact]
public void RingPyramid_FarBlockDegradesSideCellCountAndNeverAttachesBuildings()
{
var assembler = new WalkLandscapeAssembler();
var building = new WalkBuildingFactory.Entry(
new WalkBuilding { PositionCellId = (LandblockId & 0xFFFF0000u) | 1u },
Matrix4x4.Identity, Matrix4x4.Identity);
assembler.PublishLandblock(LandblockId, 1f, 0f, new[] { building });
// Three blocks north of the camera's own block -> ring 3 -> SideCellCount 2.
const uint FarLandblockId = 0xA9B7FFFFu;
assembler.PublishLandblock(FarLandblockId, 1f, 0f, new[] { building });
assembler.SetViewer(CameraCellId, Vector3.Zero);
WalkLandBlock center = assembler.Landscape.Blocks[CenterIndex()]!;
WalkLandBlock far = assembler.Landscape.Blocks[
GridIndex(WalkLandscapeAssembler.MidRadius, WalkLandscapeAssembler.MidRadius + 3)]!;
Assert.Equal(8, center.SideCellCount);
Assert.Contains(center.CellBuildings, b => b is not null);
Assert.Equal(2, far.SideCellCount);
Assert.All(far.CellBuildings, Assert.Null);
}
[Fact]
public void RetireLandblock_NullsTheWindowedSlot()
{
var assembler = new WalkLandscapeAssembler();
assembler.PublishLandblock(LandblockId, 1f, 0f, Array.Empty<WalkBuildingFactory.Entry>());
assembler.SetViewer(CameraCellId, Vector3.Zero);
Assert.NotNull(assembler.Landscape.Blocks[CenterIndex()]);
assembler.RetireLandblock(LandblockId);
Assert.Null(assembler.Landscape.Blocks[CenterIndex()]);
}
[Fact]
public void RetireLandblock_UnpublishedLandblockIsANoOp()
{
var assembler = new WalkLandscapeAssembler();
assembler.RetireLandblock(LandblockId);
Assert.Null(assembler.Landscape.Blocks[CenterIndex()]);
}
[Fact]
public void SetViewer_RecentresTheWindowWhenTheCameraCrossesIntoAnotherBlock()
{
var assembler = new WalkLandscapeAssembler();
assembler.PublishLandblock(LandblockId, 7f, -1f, Array.Empty<WalkBuildingFactory.Entry>());
assembler.SetViewer(CameraCellId, Vector3.Zero);
Assert.NotNull(assembler.Landscape.Blocks[CenterIndex()]);
// Move the camera one block east; the published landblock should now
// sit one grid slot WEST of center instead of at center.
assembler.SetViewer(0xAAB40001u, Vector3.Zero);
Assert.Null(assembler.Landscape.Blocks[CenterIndex()]);
WalkLandBlock? shifted = assembler.Landscape.Blocks[
GridIndex(WalkLandscapeAssembler.MidRadius - 1, WalkLandscapeAssembler.MidRadius)];
Assert.NotNull(shifted);
Assert.Equal(7f, shifted!.MaxZ);
}
[Fact]
public void SetViewer_LandcellIndexDerivesViewerCellFromLowWord()
{
var assembler = new WalkLandscapeAssembler();
// Low word 10 -> landcell index 9 -> (9/8, 9%8) = (1, 1).
assembler.SetViewer(0xA9B4000Au, Vector3.Zero);
Assert.Equal(1, assembler.Landscape.ViewerCellX);
Assert.Equal(1, assembler.Landscape.ViewerCellY);
}
[Fact]
public void SetViewer_InteriorCameraDerivesViewerCellFromOrigin()
{
var assembler = new WalkLandscapeAssembler();
assembler.SetViewer(0xA9B40105u, new Vector3(50f, 74f, 0f));
Assert.Equal(2, assembler.Landscape.ViewerCellX); // floor(50 / 24) = 2
Assert.Equal(3, assembler.Landscape.ViewerCellY); // floor(74 / 24) = 3
}
[Fact]
public void SetViewer_SameBlockRepeatCallDoesNotClearAlreadyPublishedSlots()
{
var assembler = new WalkLandscapeAssembler();
assembler.PublishLandblock(LandblockId, 1f, 0f, Array.Empty<WalkBuildingFactory.Entry>());
assembler.SetViewer(CameraCellId, Vector3.Zero);
assembler.SetViewer(CameraCellId, new Vector3(5f, 5f, 0f));
Assert.NotNull(assembler.Landscape.Blocks[CenterIndex()]);
}
}

View file

@ -0,0 +1,131 @@
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Walk;
namespace AcDream.App.Tests.Rendering.Walk;
/// <summary>Campaign FW3.1 — hermetic (no DAT) coverage of the production
/// frame context: cell resolution through <see cref="CellVisibility"/>,
/// building resolution through <see cref="WalkBuildingRegistry"/>, and the
/// CyPlane/ray-cast wiring. This proves the SEAM (does the context read the
/// right registries the right way); the ten-fixture conformance gate proves
/// the WORLD DATA those registries are fed is correct.</summary>
public sealed class WalkProductionFrameContextTests
{
private static Matrix4x4 SimpleViewProjection() =>
Matrix4x4.CreateLookAt(Vector3.Zero, Vector3.UnitY, Vector3.UnitZ)
* Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 4f / 3f, 0.1f, 1000f);
[Fact]
public void GetVisible_ResolvesThroughTheCommittedCellVisibilityRegistry()
{
var cellVisibility = new CellVisibility();
var walkCell = new WalkCell { CellId = 0xA9B40100u };
var loaded = new LoadedCell { CellId = 0xA9B40100u, Walk = walkCell };
cellVisibility.CommitLandblock(0xA9B4FFFFu, new[] { loaded });
var ctx = new WalkProductionFrameContext(
cellVisibility, new WalkBuildingRegistry(), Vector3.Zero, Vector3.UnitY,
SimpleViewProjection(), 1024f, 768f);
Assert.Same(walkCell, ctx.GetVisible(0xA9B40100u));
Assert.Null(ctx.GetVisible(0xA9B40101u));
}
[Fact]
public void GetVisible_ReturnsNullWhenTheCommittedCellHasNoWalkModel()
{
// A hand-built LoadedCell that never went through
// EnvCellLandblockBuildBuilder.BuildVisibilityCell (test-only
// shortcut some existing fixtures take) — Walk stays null.
var cellVisibility = new CellVisibility();
var loaded = new LoadedCell { CellId = 0xA9B40100u };
cellVisibility.CommitLandblock(0xA9B4FFFFu, new[] { loaded });
var ctx = new WalkProductionFrameContext(
cellVisibility, new WalkBuildingRegistry(), Vector3.Zero, Vector3.UnitY,
SimpleViewProjection(), 1024f, 768f);
Assert.Null(ctx.GetVisible(0xA9B40100u));
}
[Fact]
public void ObjectToClipAndViewpointIn_UseTheCellsOwnTransforms()
{
var cell = new WalkCell
{
CellId = 1,
WorldTransform = Matrix4x4.CreateTranslation(10f, 0f, 0f),
InverseWorldTransform = Matrix4x4.CreateTranslation(-10f, 0f, 0f),
};
Matrix4x4 vp = SimpleViewProjection();
var ctx = new WalkProductionFrameContext(
new CellVisibility(), new WalkBuildingRegistry(), new Vector3(10f, 0f, 0f), Vector3.UnitY,
vp, 1024f, 768f);
Assert.Equal(cell.WorldTransform * vp, ctx.ObjectToClip(cell));
Assert.Equal(Vector3.Zero, ctx.ViewpointIn(cell));
}
[Fact]
public void ViewpointInBuilding_ResolvesThroughWalkBuildingRegistry()
{
var registry = new WalkBuildingRegistry();
var building = new WalkBuilding { PositionCellId = 1 };
Matrix4x4 world = Matrix4x4.CreateTranslation(5f, 0f, 0f);
Matrix4x4.Invert(world, out Matrix4x4 inverse);
registry.Publish(0xA9B4FFFFu, new[] { new WalkBuildingFactory.Entry(building, world, inverse) });
var ctx = new WalkProductionFrameContext(
new CellVisibility(), registry, new Vector3(5f, 0f, 0f), Vector3.UnitY,
SimpleViewProjection(), 1024f, 768f);
Assert.Equal(Vector3.Zero, ctx.ViewpointInBuilding(building));
}
[Fact]
public void ViewpointInBuilding_ThrowsWhenTheBuildingIsNotCommitted()
{
// Fail loud (the PV3 post-mortem rule): a walk/registry desync must
// never resolve to a silently-skipped building.
var ctx = new WalkProductionFrameContext(
new CellVisibility(), new WalkBuildingRegistry(), Vector3.Zero, Vector3.UnitY,
SimpleViewProjection(), 1024f, 768f);
var unregistered = new WalkBuilding { PositionCellId = 1 };
Assert.Throws<InvalidOperationException>(() => ctx.ViewpointInBuilding(unregistered));
}
[Fact]
public void ViewerDistanceTo_MeasuresToTheBuildingsTransformedSortCenter()
{
var registry = new WalkBuildingRegistry();
var building = new WalkBuilding { PositionCellId = 1, SortCenter = new Vector3(0f, 3f, 0f) };
Matrix4x4 world = Matrix4x4.CreateTranslation(0f, 10f, 0f);
Matrix4x4.Invert(world, out Matrix4x4 inverse);
registry.Publish(0xA9B4FFFFu, new[] { new WalkBuildingFactory.Entry(building, world, inverse) });
var ctx = new WalkProductionFrameContext(
new CellVisibility(), registry, Vector3.Zero, Vector3.UnitY,
SimpleViewProjection(), 1024f, 768f);
Assert.Equal(13f, ctx.ViewerDistanceTo(building));
}
[Fact]
public void CyPlane_MatchesTheRetailNearPlaneFormula()
{
Vector3 forward = Vector3.UnitY;
var eye = new Vector3(0f, 5f, 0f);
var ctx = new WalkProductionFrameContext(
new CellVisibility(), new WalkBuildingRegistry(), eye, forward,
SimpleViewProjection(), 1024f, 768f);
Assert.Equal(forward, ctx.CyPlane.Normal);
Assert.Equal(-Vector3.Dot(eye, forward) - WalkProductionFrameContext.ZNear, ctx.CyPlane.D);
}
[Fact]
public void Constructor_RejectsANonInvertibleViewProjection()
{
Assert.Throws<ArgumentException>(() => new WalkProductionFrameContext(
new CellVisibility(), new WalkBuildingRegistry(), Vector3.Zero, Vector3.UnitY,
default, 1024f, 768f));
}
}

View file

@ -0,0 +1,321 @@
using AcDream.App.Rendering;
using AcDream.App.Tests.Rendering;
using AcDream.App.Rendering.Walk;
using AcDream.Content;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Options;
using System.Numerics;
namespace AcDream.App.Tests.Rendering.Walk;
/// <summary>
/// Campaign FW3.1's load-bearing deliverable: the ten pose-stamped oracle
/// fixtures (docs/research/2026-08-30-fw-walk-oracle/), replayed through the
/// PRODUCTION world-data builders
/// (<see cref="WalkCellFactory"/>/<see cref="WalkBuildingFactory"/>
/// /<see cref="WalkLandscapeAssembler"/> — the same code
/// <c>EnvCellLandblockBuildBuilder</c>/<c>LandblockBuildFactory</c> run at
/// real landblock-build time, consuming ONLY the legal
/// <see cref="IDatReaderWriter"/> seam), must reproduce the identical walk
/// output <see cref="WalkTraceConformanceTests"/> already proves against the
/// FW1 test adapter (<c>WalkWorldDatAdapter</c>/<c>WalkLandscapeDatBuilder</c>).
///
/// This class deliberately does NOT touch <see cref="WalkTraceConformanceTests"/>
/// (frozen — must stay green untouched) but reuses its driver/signature
/// helpers verbatim: <see cref="WalkTraceReplayContext"/>,
/// <see cref="WalkOracleTrace"/>, <see cref="WalkOraclePose"/>,
/// <see cref="WalkOracleFrame"/>. <see cref="WalkTraceReplayContext.Buildings"/>
/// is typed against the TEST adapter's <c>WalkWorldDatAdapter.BuildingEntry</c>
/// record — structurally identical to <see cref="WalkBuildingFactory.Entry"/>
/// (same three fields) — so <see cref="BuildProductionWorld"/> below adapts
/// one into the other rather than touching the shared context type.
///
/// <see cref="BuildProductionWorld"/> mirrors the FW1 harness's
/// <c>WalkLandscapeDatBuilder.Build</c> loop structure and ring math
/// EXACTLY (51×51 grid, <c>WalkLandscapeAssembler.MidRadius</c> = 25,
/// buildings/stab-cells only at full resolution), but every DATA-BUILDING
/// step below it — cells, buildings, the drawing BSP, z-slab — calls the
/// PRODUCTION functions. That is the whole conformance claim: the grid/ring
/// assembly is proven once (FW1's existing gate, via
/// <see cref="WalkLandscapeAssembler"/>'s own port of that same math); this
/// class proves the DATA those slots are filled with is identical to the
/// FW1 test adapter's.
/// </summary>
[Trait("Lane", "InstalledDat")]
public sealed class WalkProductionWorldConformanceTests
{
private sealed class Recorder : IWalkEventSink
{
public readonly List<WalkEvent> Events = new();
public void Emit(in WalkEvent walkEvent) => Events.Add(walkEvent);
}
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);
}
/// <summary>Builds the walk's world data (landscape + interior cells +
/// buildings) for a camera pose through the PRODUCTION builders, in the
/// exact shape <see cref="WalkTraceReplayContext"/> expects. Mirrors
/// <c>WalkLandscapeDatBuilder.Build</c>'s grid loop; every cell/building
/// constructed inside it comes from <see cref="WalkCellFactory"/> /
/// <see cref="WalkBuildingFactory"/>.</summary>
private static (WalkLandscapeAssembler Assembler, Dictionary<uint, WalkCell> Cells,
Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> Buildings)
BuildProductionWorld(IDatReaderWriter dats, uint cameraCellId, Vector3 cameraOrigin)
{
int cameraBlockX = (int)(cameraCellId >> 24);
int cameraBlockY = (int)((cameraCellId >> 16) & 0xFF);
var assembler = new WalkLandscapeAssembler();
var cells = new Dictionary<uint, WalkCell>();
var buildings = new Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry>();
Region region = (Region)dats.Get<Region>(0x13000000u)!;
float[] heightTable = region.LandDefs.LandHeightTable;
for (int gx = 0; gx < WalkLandscapeAssembler.GridWidth; gx++)
{
for (int gy = 0; gy < WalkLandscapeAssembler.GridWidth; gy++)
{
int blockX = cameraBlockX + gx - WalkLandscapeAssembler.MidRadius;
int blockY = cameraBlockY + gy - WalkLandscapeAssembler.MidRadius;
if (blockX < 0 || blockX > 0xFF || blockY < 0 || blockY > 0xFF)
continue;
uint landblockId = (uint)((blockX << 24) | (blockY << 16));
if (dats.Get<LandBlock>(landblockId | 0xFFFFu) is not LandBlock landBlock)
continue;
byte maxByte = 0, minByte = 255;
foreach (byte h in landBlock.Height)
{
if (h > maxByte) maxByte = h;
if (h < minByte) minByte = h;
}
float maxZ = heightTable[maxByte] + 200f;
float minZ = heightTable[minByte] - 1f;
var blockOffset = new Vector3(
(gx - WalkLandscapeAssembler.MidRadius) * WalkLandscape.BlockLength,
(gy - WalkLandscapeAssembler.MidRadius) * WalkLandscape.BlockLength,
0f);
var blockBuildings = new List<WalkBuildingFactory.Entry>();
if (WalkLandscapeAssembler.SideCellCountForRing(WalkLandscapeAssembler.RingOf(gx, gy)) == 8)
{
LandBlockInfo? info = dats.Get<LandBlockInfo>(landblockId | 0xFFFEu);
blockBuildings = WalkBuildingFactory.Build(dats, landblockId, info?.Buildings, blockOffset);
foreach (WalkBuildingFactory.Entry entry in blockBuildings)
{
buildings[entry.Building] = new WalkWorldDatAdapter.BuildingEntry(
entry.Building, entry.WorldTransform, entry.InverseWorldTransform);
// Retail's loaded-interior rule (CLandBlock::init_buildings
// @0052fd80 -> add_to_stablist -> grab_visible_cells): a
// full-res block loads exactly its buildings' portal stab
// cells, matching WalkLandscapeDatBuilder's harness scope.
foreach (WalkBldPortal portal in entry.Building.Portals)
{
if (portal.OtherCellId != 0xFFFFFFFFu && !cells.ContainsKey(portal.OtherCellId))
{
WalkCell? c = WalkCellFactory.BuildCell(dats, portal.OtherCellId, blockOffset);
if (c is not null) cells[c.CellId] = c;
}
foreach (uint stab in portal.StabList)
{
if (cells.ContainsKey(stab)) continue;
WalkCell? c = WalkCellFactory.BuildCell(dats, stab, blockOffset);
if (c is not null) cells[c.CellId] = c;
}
}
}
}
assembler.PublishLandblock(landblockId, maxZ, minZ, blockBuildings);
}
}
assembler.SetViewer(cameraCellId, cameraOrigin);
return (assembler, cells, buildings);
}
[Fact]
public void Street_outdoor_first_frame_diff()
{
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Load("posed/holtburg-street-outdoor");
Assert.NotEmpty(frames);
using DatCollection dats = OpenDats();
using var adapter = new DatCollectionAdapter(dats);
WalkOracleFrame frame = frames[1];
Assert.NotNull(frame.Pose);
(WalkLandscapeAssembler assembler, Dictionary<uint, WalkCell> cells,
Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> buildings) =
BuildProductionWorld(adapter, frame.Pose!.CellId, frame.Pose.Origin);
var ctx = new WalkTraceReplayContext(frame.Pose, cells) { Buildings = buildings };
var walk = new RetailFrameWalk();
var recorder = new Recorder();
walk.WalkFrame(frame.Pose.CellId, null, assembler.Landscape, ctx, recorder);
string expected = WalkTraceReplayContext.Signature(frame);
string actual = WalkTraceReplayContext.Signature(recorder.Events);
Assert.True(
expected == actual,
$"production walk diverged from the FW1 test adapter\nEXPECTED: {expected}\nACTUAL: {actual}");
}
[Fact]
public void Doorway_still_first_frame_diff()
{
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Load("posed/holtburg-doorway-still");
Assert.NotEmpty(frames);
using DatCollection dats = OpenDats();
using var adapter = new DatCollectionAdapter(dats);
WalkOracleFrame frame = frames[1];
Assert.NotNull(frame.Pose);
(WalkLandscapeAssembler assembler, Dictionary<uint, WalkCell> cells,
Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> buildings) =
BuildProductionWorld(adapter, frame.Pose!.CellId, frame.Pose.Origin);
var ctx = new WalkTraceReplayContext(frame.Pose, cells) { Buildings = buildings };
WalkCell camera = Assert.Contains(frame.Pose.CellId, cells);
// Matches WalkTraceConformanceTests.Doorway_still_first_frame_diff:
// this capture ran under cdb load with Render::deg_mul depressed to
// the portless-arm threshold. Same environment pin, same reason.
var walk = new RetailFrameWalk { DegradeMultiplier = 0f };
var recorder = new Recorder();
walk.WalkFrame(frame.Pose.CellId, camera, assembler.Landscape, ctx, recorder);
string expected = WalkTraceReplayContext.Signature(frame);
string actual = WalkTraceReplayContext.Signature(recorder.Events);
Assert.True(
expected == actual,
$"production walk diverged from the FW1 test adapter\nEXPECTED: {expected}\nACTUAL: {actual}");
}
[Theory]
[InlineData("posed/terrace-center")]
[InlineData("posed/terrace-edge")]
[InlineData("posed/cathedral-arrival")]
public void Still_fixture_first_frame_reproduces_exactly(string fixture)
{
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Load(fixture);
Assert.NotEmpty(frames);
using DatCollection dats = OpenDats();
using var adapter = new DatCollectionAdapter(dats);
WalkOracleFrame frame = frames[1];
Assert.NotNull(frame.Pose);
(WalkLandscapeAssembler assembler, Dictionary<uint, WalkCell> cells,
Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> buildings) =
BuildProductionWorld(adapter, frame.Pose!.CellId, frame.Pose.Origin);
var ctx = new WalkTraceReplayContext(frame.Pose, cells) { Buildings = buildings };
WalkCell? camera = (frame.Pose.CellId & 0xFFFFu) >= 0x100
? Assert.Contains(frame.Pose.CellId, cells)
: null;
var walk = new RetailFrameWalk();
var recorder = new Recorder();
walk.WalkFrame(frame.Pose.CellId, camera, assembler.Landscape, ctx, recorder);
string expected = WalkTraceReplayContext.Signature(frame);
string actual = WalkTraceReplayContext.Signature(recorder.Events);
Assert.True(
expected == actual,
$"production walk diverged from the FW1 test adapter ({fixture})\nEXPECTED: {expected}\nACTUAL: {actual}");
}
[Fact]
public void Foundry_entry_reproduces_every_frame_before_the_f67_order_segment()
{
// Same F67-F79 parked segment as WalkTraceConformanceTests (the
// building-a9b40036-root-plane viewpoint question is a walk/ported-
// algorithm question, not a world-data question — out of scope for
// this class). Frames 1-66 must still reproduce exactly.
MovingFixtureReplay("posed/foundry-entry", stopBeforeFrame: 67);
}
[Theory]
[InlineData("posed/holtburg-walkout")]
[InlineData("posed/holtburg-transitions")]
[InlineData("posed/holtburg-walkabout")]
public void Moving_fixture_reproduces_every_pairable_frame(string fixture)
=> MovingFixtureReplay(fixture);
private void MovingFixtureReplay(string fixture, int stopBeforeFrame = int.MaxValue)
{
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Load(fixture);
Assert.True(frames.Count >= 3);
using DatCollection dats = OpenDats();
using var adapter = new DatCollectionAdapter(dats);
WalkOraclePose anchor = frames[1].Pose!;
(WalkLandscapeAssembler assembler, Dictionary<uint, WalkCell> cells,
Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> buildings) =
BuildProductionWorld(adapter, anchor.CellId, anchor.Origin);
var walk = new RetailFrameWalk();
for (int n = 1; n < frames.Count - 1; n++)
{
WalkOracleFrame frame = frames[n];
if (frame.Number >= stopBeforeFrame) break;
string expected = WalkTraceReplayContext.Signature(frame);
string? firstActual = null;
bool matched = false;
foreach (WalkOraclePose pose in new[] { frames[n + 1].Pose!, frame.Pose! })
{
Assert.NotNull(pose);
assembler.SetViewer(pose.CellId, pose.Origin);
var ctx = new WalkTraceReplayContext(pose, cells) { Buildings = buildings };
WalkCell? camera = null;
if ((pose.CellId & 0xFFFFu) >= 0x100)
{
Assert.True(
cells.TryGetValue(pose.CellId, out camera),
$"frame {frame.Number}: interior camera cell {pose.CellId:x8} not loaded");
}
var recorder = new Recorder();
walk.WalkFrame(pose.CellId, camera, assembler.Landscape, ctx, recorder);
string actual = WalkTraceReplayContext.Signature(recorder.Events);
firstActual ??= actual;
if (actual == expected)
{
matched = true;
break;
}
}
Assert.True(
matched,
$"frame {frame.Number} diverged under both adjacent poses ({fixture})\n"
+ $"EXPECTED: {expected}\nACTUAL: {firstActual}");
}
}
[Fact]
public void Foundry_deep_reproduces_every_complete_frame_exactly()
{
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Load("posed/foundry-deep");
Assert.NotEmpty(frames);
using DatCollection dats = OpenDats();
using var adapter = new DatCollectionAdapter(dats);
Dictionary<uint, WalkCell> cells =
WalkCellFactory.BuildInteriorCells(adapter, 0xA9B40000u, Vector3.Zero);
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
foreach (WalkOracleFrame frame in frames)
{
Assert.NotNull(frame.Pose);
WalkCell camera = Assert.Contains(frame.Pose!.CellId, cells);
var ctx = new WalkTraceReplayContext(frame.Pose, cells);
var walk = new RetailFrameWalk();
var recorder = new Recorder();
walk.WalkFrame(frame.Pose.CellId, camera, landscape, ctx, recorder);
Assert.Equal(
WalkTraceReplayContext.Signature(frame),
WalkTraceReplayContext.Signature(recorder.Events));
}
}
}

View file

@ -89,6 +89,74 @@ public sealed class LandblockBuildFactoryTests
Assert.Empty(result.Collisions.EnvCells);
}
[Fact]
public void BuildNear_PopulatesWalkZSlabUnconditionallyAndWalkBuildingsFromLandBlockInfo()
{
// Campaign FW3.1: EnvCellLandblockBuild.WalkMaxZ/WalkMinZ/WalkBuildings
// must be populated by the production streaming build — hermetic
// (no installed DAT needed), mirroring the RecordingDatProxy fixture
// pattern the rest of this file already uses.
var dat = CreateDat(out RecordingDatProxy proxy);
AddNearFixture(proxy, LandblockId, environmentId: 1);
var heights = new byte[81];
Array.Fill(heights, (byte)10);
heights[0] = 200; // one outlier byte -> a distinct maxByte from the rest.
proxy.Add(LandblockId, new LandBlock { Id = LandblockId, Height = heights });
proxy.Add(
(LandblockId & 0xFFFF0000u) | 0xFFFEu,
new LandBlockInfo
{
NumCells = 1,
Buildings = new List<BuildingInfo>
{
new BuildingInfo
{
ModelId = 0x01234567u,
Frame = new Frame
{
Origin = new Vector3(12f, 12f, 0f),
Orientation = Quaternion.Identity,
},
Portals = new List<BuildingPortal>(),
},
},
});
var heightTable = new float[256];
heightTable[10] = 5f;
heightTable[200] = 40f;
var factory = new LandblockBuildFactory(
dat, TestPreparedCollisionSource.Instance, new object(), heightTable);
LandblockBuild? result = factory.Build(Request(LandblockStreamJobKind.LoadNear));
Assert.NotNull(result);
var envCells = Assert.IsType<AcDream.App.Rendering.Wb.EnvCellLandblockBuild>(
result.EnvCells);
Assert.Equal(240f, envCells.WalkMaxZ); // heightTable[200] + 200
Assert.Equal(4f, envCells.WalkMinZ); // heightTable[10] - 1
AcDream.App.Rendering.Walk.WalkBuildingFactory.Entry buildingEntry =
Assert.Single(envCells.WalkBuildings);
// origin (12,12) -> cellX=cellY=0 -> low word 0*8+0+1 = 1.
Assert.Equal((LandblockId & 0xFFFF0000u) | 1u, buildingEntry.Building.PositionCellId);
}
[Fact]
public void BuildFar_NeverPopulatesWalkDataBecauseEnvCellsIsNull()
{
// Documents the known FW3.1 scope gap: far-tier (LoadFar) landblocks
// carry no EnvCellLandblockBuild transaction at all (terrain-only),
// so their z-slab/buildings never reach WalkLandscapeAssembler until
// a follow-up wires the far-tier path too.
var dat = CreateDat(out RecordingDatProxy proxy);
proxy.Add(LandblockId, new LandBlock { Id = LandblockId });
var factory = Factory(dat, new object());
LandblockBuild? result = factory.Build(Request(LandblockStreamJobKind.LoadFar));
Assert.NotNull(result);
Assert.Null(result.EnvCells);
}
[Fact]
public void NearPreparedFaultRejectsWholeGenerationWhileFarNeverReadsCollision()
{