acdream/src/AcDream.App/Rendering/Walk/WalkBuildingRegistry.cs
Erik b10ad662b0 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>
2026-08-30 13:19:22 +02:00

75 lines
3.7 KiB
C#

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