using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering;
using DatReaderWriter.DBObjs;
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).
///
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).
///
/// The dat-loaded for this landblock.
/// The 32-bit landblock id (e.g. 0xA9B40000).
/// The high 16 bits are ORed with each 16-bit OtherCellId to produce
/// the full cell id.
/// Pre-loaded cells keyed by full 32-bit cell id.
/// Used for BFS extension (Step B) and exit-polygon collection (Step C).
/// An empty dict is valid for unit tests; Step B and C are skipped per cell.
/// A fully populated ; never null.
public static BuildingRegistry Build(
LandBlockInfo info,
uint landblockId,
IReadOnlyDictionary cellsByCellId)
{
var reg = new BuildingRegistry();
if (info.Buildings is null || info.Buildings.Count == 0)
return reg;
uint lbMask = landblockId & 0xFFFF0000u;
uint nextId = 1;
foreach (var bInfo in info.Buildings)
{
var envCellIds = new HashSet();
var exitPortalPolys = new List();
// Step A: seed the cell set from BuildingInfo.Portals (entry portals).
// Each BuildingPortal.OtherCellId is a 16-bit cell-local id; OR with
// the landblock prefix for the full id.
// Defensive: skip OtherCellId == 0xFFFF (exit-portal sentinel in
// BuildingInfo — rare but WB guards against it too at PortalService.cs:58).
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 CellPortals to find the full cell set.
// Uses pre-loaded LoadedCell.Portals (avoids a duplicate dat fetch per
// BFS step). Mirrors WB PortalService.cs:67-79.
// When cellsByCellId is empty (unit-test path), BFS immediately exits.
var queue = new Queue(envCellIds);
while (queue.Count > 0)
{
var current = queue.Dequeue();
if (!cellsByCellId.TryGetValue(current, out var cell)) continue;
foreach (var p in cell.Portals)
{
if (p.OtherCellId == 0xFFFF) continue; // exit portal — stop BFS here
uint neighbourId = lbMask | p.OtherCellId;
if (envCellIds.Add(neighbourId))
queue.Enqueue(neighbourId);
}
}
// Step C: collect exit portal polygons in world space.
// For each interior cell, iterate its portals; for each exit portal
// (OtherCellId == 0xFFFF), transform the portal polygon vertices from
// cell-local space to world space via WorldTransform.
// Mirrors WB PortalService.cs:81-86 (GetPortalsForCell return path).
foreach (var cellId in envCellIds)
{
if (!cellsByCellId.TryGetValue(cellId, out var cell)) continue;
for (int pi = 0; pi < cell.Portals.Count; pi++)
{
if (cell.Portals[pi].OtherCellId != 0xFFFF) continue;
if (pi >= cell.PortalPolygons.Count) continue;
var localPoly = cell.PortalPolygons[pi];
if (localPoly.Length < 3) continue;
var worldPoly = new Vector3[localPoly.Length];
for (int v = 0; v < localPoly.Length; v++)
worldPoly[v] = Vector3.Transform(localPoly[v], cell.WorldTransform);
exitPortalPolys.Add(worldPoly);
}
}
// WB PortalService.cs:89: skip buildings with no interior cells.
if (envCellIds.Count == 0) continue;
var building = new Building
{
BuildingId = nextId++,
EnvCellIds = envCellIds,
ExitPortalPolygons = exitPortalPolys,
};
reg.Add(building);
// Step 4: stamp BuildingId on each cell (Option C — both directions
// O(1)). The internal setter on LoadedCell.BuildingId is accessible
// because this class lives in the same assembly (AcDream.App).
foreach (var cellId in envCellIds)
{
if (cellsByCellId.TryGetValue(cellId, out var cell))
cell.BuildingId = building.BuildingId;
}
}
return reg;
}
}