using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Types;
namespace AcDream.App.Rendering.Wb;
///
/// Phase A8 (2026-05-26): static factory that builds a per-landblock
/// from a 's
/// Buildings array.
///
/// Algorithm (mirrors WB's PortalService.GetPortalsByBuilding at
/// WorldBuilder.Shared/Services/PortalService.cs:43-97):
///
/// - Step A — seed the cell set from BuildingInfo.Portals entry portals.
/// - Step B — BFS through to discover all
/// interior cells reachable from the entry portals (interior portals only;
/// exit portals — OtherCellId == 0xFFFF — terminate each BFS branch).
/// - Step C — collect exit portal polygons in world space for the stencil
/// pipeline (Phase A8 Steps 1+2, RR7 scope).
///
///
/// Cells whose LoadedCell entries are missing from
/// are silently skipped (BFS bails at the
/// unloaded cell). In production, streaming loads all cells for a landblock
/// before runs, so the dict is always complete.
///
/// LoadedCell.BuildingId is stamped here in RR4 after reg.Add(building).
///
/// Retail references:
/// docs/research/named-retail/acclient.h:32035 (BuildInfo) and
/// :32094 (CBldPortal).
///
internal sealed class BuildingRegistryPublication
{
internal BuildingRegistryPublication(
BuildingInfo[] buildings,
uint landblockId,
IReadOnlyDictionary cellsByCellId)
{
Buildings = buildings;
LandblockId = landblockId;
CellsByCellId = cellsByCellId;
PreparationCommitted = buildings.Length == 0;
}
internal BuildingInfo[] Buildings { get; }
internal uint LandblockId { get; }
internal IReadOnlyDictionary CellsByCellId { get; }
internal BuildingRegistry Registry { get; } = new();
internal List<(LoadedCell Cell, uint BuildingId)> CellStamps { get; } = new();
internal int BuildingCursor { get; set; }
internal uint NextBuildingId { get; set; } = 1;
internal bool PreparationCommitted { get; set; }
internal bool PublicationCommitted { get; set; }
}
public static class BuildingLoader
{
///
/// Builds a from the supplied landblock data.
/// Building IDs are allocated sequentially starting at 1 (0 is reserved for
/// "no building" semantics used by LoadedCell.BuildingId in RR4).
///
public static BuildingRegistry Build(
LandBlockInfo info,
uint landblockId,
IReadOnlyDictionary cellsByCellId)
{
BuildingRegistryPublication publication = PreparePublication(
info,
landblockId,
cellsByCellId);
while (!AdvancePreparationOne(publication))
{
}
CommitPublication(publication);
return publication.Registry;
}
internal static BuildingRegistryPublication PreparePublication(
LandBlockInfo info,
uint landblockId,
IReadOnlyDictionary cellsByCellId)
{
ArgumentNullException.ThrowIfNull(info);
ArgumentNullException.ThrowIfNull(cellsByCellId);
int buildingCount = info.Buildings?.Count ?? 0;
var buildings = new BuildingInfo[buildingCount];
for (int index = 0; index < buildingCount; index++)
buildings[index] = info.Buildings![index];
return new BuildingRegistryPublication(
buildings,
landblockId,
cellsByCellId);
}
internal static bool AdvancePreparationOne(
BuildingRegistryPublication publication)
{
ArgumentNullException.ThrowIfNull(publication);
if (publication.PreparationCommitted)
return true;
if (publication.BuildingCursor >= publication.Buildings.Length)
{
publication.PreparationCommitted = true;
return true;
}
AddBuilding(
publication,
publication.Buildings[publication.BuildingCursor]);
publication.BuildingCursor++;
if (publication.BuildingCursor >= publication.Buildings.Length)
publication.PreparationCommitted = true;
return publication.PreparationCommitted;
}
internal static void CommitPublication(
BuildingRegistryPublication publication)
{
ArgumentNullException.ThrowIfNull(publication);
if (!publication.PreparationCommitted)
{
throw new InvalidOperationException(
"A building registry cannot publish before preparation completes.");
}
if (publication.PublicationCommitted)
return;
foreach ((LoadedCell cell, uint buildingId) in publication.CellStamps)
cell.BuildingId = buildingId;
publication.PublicationCommitted = true;
}
private static void AddBuilding(
BuildingRegistryPublication publication,
BuildingInfo bInfo)
{
uint lbMask = publication.LandblockId & 0xFFFF0000u;
var envCellIds = new HashSet();
var exitPortalPolys = new List();
// Step A: seed the cell set from BuildingInfo.Portals.
if (bInfo.Portals is not null)
{
foreach (var portal in bInfo.Portals)
{
if (portal.OtherCellId == 0xFFFF) continue;
envCellIds.Add(lbMask | portal.OtherCellId);
}
}
// Step B: BFS through interior portals.
var queue = new Queue(envCellIds);
while (queue.Count > 0)
{
uint current = queue.Dequeue();
if (!publication.CellsByCellId.TryGetValue(current, out var cell))
continue;
foreach (var portal in cell.Portals)
{
if (portal.OtherCellId == 0xFFFF) continue;
uint neighbourId = lbMask | portal.OtherCellId;
if (envCellIds.Add(neighbourId))
queue.Enqueue(neighbourId);
}
}
// Step C: collect exit portal polygons in world space.
foreach (uint cellId in envCellIds)
{
if (!publication.CellsByCellId.TryGetValue(cellId, out var cell))
continue;
for (int portalIndex = 0; portalIndex < cell.Portals.Count; portalIndex++)
{
if (cell.Portals[portalIndex].OtherCellId != 0xFFFF) continue;
if (portalIndex >= cell.PortalPolygons.Count) continue;
Vector3[] localPolygon = cell.PortalPolygons[portalIndex];
if (localPolygon.Length < 3) continue;
var worldPolygon = new Vector3[localPolygon.Length];
for (int vertexIndex = 0; vertexIndex < localPolygon.Length; vertexIndex++)
{
worldPolygon[vertexIndex] = Vector3.Transform(
localPolygon[vertexIndex],
cell.WorldTransform);
}
exitPortalPolys.Add(worldPolygon);
}
}
bool hasPortalBounds = false;
var portalMin = new Vector3(float.MaxValue);
var portalMax = new Vector3(float.MinValue);
foreach (Vector3[] polygon in exitPortalPolys)
{
foreach (Vector3 vertex in polygon)
{
hasPortalBounds = true;
portalMin = Vector3.Min(portalMin, vertex);
portalMax = Vector3.Max(portalMax, vertex);
}
}
if (envCellIds.Count == 0)
return;
uint buildingId = publication.NextBuildingId++;
publication.Registry.Add(new Building
{
BuildingId = buildingId,
EnvCellIds = envCellIds,
ExitPortalPolygons = exitPortalPolys,
HasPortalBounds = hasPortalBounds,
PortalBounds = hasPortalBounds
? new WbBoundingBox(portalMin, portalMax)
: default,
});
// Stamps remain off-side until the complete registry commits.
foreach (uint cellId in envCellIds)
{
if (publication.CellsByCellId.TryGetValue(cellId, out var cell))
publication.CellStamps.Add((cell, buildingId));
}
}
}