acdream/src/AcDream.App/Rendering/Wb/BuildingRegistry.cs
Erik f125fdb220 feat(render): Phase A8 RR3 — Building + BuildingRegistry + BuildingLoader
New per-landblock data model for WB-style per-building cell scoping:

  Building            — BuildingId, EnvCellIds, ExitPortalPolygons,
                        occlusion-query state (Step 5 lifecycle)
  BuildingRegistry    — two-way indexed (by cellId + by buildingId);
                        single source of truth per landblock
  BuildingLoader      — static factory from LandBlockInfo.Buildings;
                        walks interior portals to expand cell sets;
                        collects exit portal polygons in world space

10 new unit tests cover data invariants + registry indexing + loader
mapping per the algorithm resolved in RR2 findings.

LoadedCell.BuildingId stamping wired in RR4. Render-time consumption
arrives in RR7 (Steps 1-4) + RR9 (Step 5) + RR11 (RenderOutsideIn).

Design: docs/superpowers/specs/2026-05-26-phase-a8-wb-full-port-design.md
Spike: docs/research/2026-05-26-a8-buildings-data-shape.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 11:08:43 +02:00

73 lines
3.2 KiB
C#

using System;
using System.Collections.Generic;
namespace AcDream.App.Rendering.Wb;
/// <summary>
/// Phase A8 (2026-05-26): per-landblock registry of <see cref="Building"/>s.
/// Two-way indexed for O(1) cell→building and building-id→building lookups.
/// Built once per landblock at load time by <see cref="BuildingLoader"/>;
/// no mutations occur after initial population.
///
/// <para>The cell→building index uses a <c>List&lt;Building&gt;</c> value type
/// to handle the (rare but valid) case where two buildings share an EnvCell —
/// each building performs its own BFS so a shared boundary cell ends up in both
/// <c>EnvCellIds</c> sets. <see cref="GetBuildingsContainingCell"/> returns all
/// owners so RR7's render path can pick the correct one.</para>
///
/// <para>WB reference: <c>WorldBuilder.Shared/Services/PortalService.cs</c>
/// (<c>BuildingPortalGroup</c>). Design:
/// <c>docs/superpowers/specs/2026-05-26-phase-a8-wb-full-port-design.md</c>.</para>
/// </summary>
public sealed class BuildingRegistry
{
// Index 1: cell-id → list of buildings containing that cell.
// Cells may belong to multiple buildings (rare; handled via List<Building>).
private readonly Dictionary<uint, List<Building>> _byCellId = new();
// Index 2: building-id → Building.
private readonly Dictionary<uint, Building> _byBuildingId = new();
/// <summary>
/// Adds a building to both indexes. Idempotent if the exact same
/// <see cref="Building"/> instance is added twice with the same
/// <see cref="Building.BuildingId"/>.
/// </summary>
/// <param name="b">The building to register.</param>
public void Add(Building b)
{
if (_byBuildingId.TryGetValue(b.BuildingId, out var existing) && ReferenceEquals(existing, b))
return;
_byBuildingId[b.BuildingId] = b;
foreach (var cellId in b.EnvCellIds)
{
if (!_byCellId.TryGetValue(cellId, out var list))
{
list = new List<Building>();
_byCellId[cellId] = list;
}
if (!list.Contains(b)) list.Add(b);
}
}
/// <summary>
/// Returns the buildings containing <paramref name="cellId"/>.
/// Returns an empty read-only list when the cell isn't part of any
/// building (outdoor cells, dungeon cells not tagged by
/// <c>LandBlockInfo.Buildings</c>).
/// </summary>
/// <param name="cellId">Full 32-bit cell id.</param>
public IReadOnlyList<Building> GetBuildingsContainingCell(uint cellId) =>
_byCellId.TryGetValue(cellId, out var list) ? list : Array.Empty<Building>();
/// <summary>Returns the building with the given id, or <c>null</c> if not found.</summary>
/// <param name="buildingId">The building id as allocated by <see cref="BuildingLoader"/>.</param>
public Building? GetById(uint buildingId) =>
_byBuildingId.TryGetValue(buildingId, out var b) ? b : null;
/// <summary>Enumerates every registered building in unspecified order.</summary>
public IEnumerable<Building> All() => _byBuildingId.Values;
/// <summary>Number of registered buildings.</summary>
public int Count => _byBuildingId.Count;
}