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

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