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

@ -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;