diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index 6b2736d..ac17ed8 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -5895,17 +5895,7 @@ public sealed class GameWindow : IDisposable
uint lbRegistryKey = lb.LandblockId & 0xFFFF0000u;
_buildingRegistries[lbRegistryKey] =
AcDream.App.Rendering.Wb.BuildingLoader.Build(
- lbInfo,
- lb.LandblockId,
- _cellVisibility.AllLoadedCells,
- // RR7.3: dat-driven BFS — completes regardless of which
- // cells have streamed into _cellVisibility by the time
- // lbInfo arrives. Without this, large multi-room
- // buildings (Holtburg Inn = 209 leaves, 2 entry portals)
- // had EnvCellIds short of the building's actual cell
- // set when intermediate cells weren't yet loaded.
- dats: _dats,
- landblockOrigin: origin);
+ lbInfo, lb.LandblockId, _cellVisibility.AllLoadedCells);
}
}
diff --git a/src/AcDream.App/Rendering/Wb/BuildingLoader.cs b/src/AcDream.App/Rendering/Wb/BuildingLoader.cs
index a6c1dc1..cca29f3 100644
--- a/src/AcDream.App/Rendering/Wb/BuildingLoader.cs
+++ b/src/AcDream.App/Rendering/Wb/BuildingLoader.cs
@@ -1,9 +1,7 @@
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering;
-using DatReaderWriter;
using DatReaderWriter.DBObjs;
-using DatReaderWriter.Types;
namespace AcDream.App.Rendering.Wb;
@@ -16,21 +14,20 @@ namespace AcDream.App.Rendering.Wb;
/// 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
+///
- 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).
-/// Walks the dat directly so BFS completes regardless of which cells happen
-/// to be pre-loaded into (RR7.3 fix —
-/// prior versions short-circuited on unloaded cells, missing large multi-
-/// room buildings like the Holtburg Inn).
+/// 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). Uses pre-loaded
-/// entries for the world transform when available;
-/// falls back to the dat-side envCell.Position when not.
-/// - Step D — stamp on pre-loaded cells.
-/// Cells loaded later get stamped by the drain hook in GameWindow.
+/// 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).
@@ -39,28 +36,21 @@ public static class BuildingLoader
{
///
/// Builds a from the supplied landblock data.
- /// Building IDs are allocated sequentially starting at 1.
+ /// 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 stamping (Step D) and as a fast path for exit-polygon collection.
- /// Cells not in the dict are looked up from if non-null.
- /// Dat collection for dat-driven BFS + fallback polygon
- /// resolution. May be null for unit tests (Step B short-circuits to the
- /// dict-only walk, matching the pre-RR7.3 behavior).
- /// World-space origin of the landblock. Used
- /// to translate dat-side cell positions for the polygon-fallback path.
- /// Unused when is null.
+ /// 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,
- DatCollection? dats = null,
- Vector3 landblockOrigin = default)
+ IReadOnlyDictionary cellsByCellId)
{
var reg = new BuildingRegistry();
if (info.Buildings is null || info.Buildings.Count == 0)
@@ -71,10 +61,14 @@ public static class BuildingLoader
foreach (var bInfo in info.Buildings)
{
- var envCellIds = new HashSet();
+ 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)
@@ -84,42 +78,46 @@ public static class BuildingLoader
}
}
- // Step B: BFS — dat-driven (RR7.3). Falls back to LoadedCell.Portals
- // when dats is null (unit-test path).
+ // 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();
- IReadOnlyList<(ushort OtherCellId, ushort PolygonId)>? neighbours =
- LookupNeighbours(current, cellsByCellId, dats);
- if (neighbours is null) continue;
-
- foreach (var (otherCellId, _) in neighbours)
+ if (!cellsByCellId.TryGetValue(current, out var cell)) continue;
+ foreach (var p in cell.Portals)
{
- if (otherCellId == 0xFFFF) continue;
- uint neighbourId = lbMask | otherCellId;
+ 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.
- // Fast path: use pre-loaded LoadedCell when available (already has
- // WorldTransform + resolved PortalPolygons).
- // Fallback path: walk the dat for cells not yet loaded so the
- // stencil mask is complete on first render after registry build.
+ // 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))
+ if (!cellsByCellId.TryGetValue(cellId, out var cell)) continue;
+ for (int pi = 0; pi < cell.Portals.Count; pi++)
{
- CollectExitPolygonsFromLoadedCell(cell, exitPortalPolys);
- }
- else if (dats is not null)
- {
- CollectExitPolygonsFromDat(cellId, dats, landblockOrigin, exitPortalPolys);
+ 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
@@ -130,9 +128,9 @@ public static class BuildingLoader
};
reg.Add(building);
- // Step D: stamp BuildingId on each pre-loaded cell. Cells loaded
- // later are stamped by the drain hook in GameWindow.cs that runs
- // _buildingRegistries.TryGetValue + GetBuildingsContainingCell.
+ // 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))
@@ -142,99 +140,4 @@ public static class BuildingLoader
return reg;
}
-
- ///
- /// RR7.3: dat-or-loaded neighbour lookup. Returns the cell's portal list as
- /// (OtherCellId, PolygonId) tuples so the BFS body can stay agnostic about
- /// the source.
- ///
- private static IReadOnlyList<(ushort OtherCellId, ushort PolygonId)>? LookupNeighbours(
- uint cellId,
- IReadOnlyDictionary cellsByCellId,
- DatCollection? dats)
- {
- if (cellsByCellId.TryGetValue(cellId, out var loaded))
- {
- var result = new (ushort, ushort)[loaded.Portals.Count];
- for (int i = 0; i < loaded.Portals.Count; i++)
- result[i] = (loaded.Portals[i].OtherCellId, loaded.Portals[i].PolygonId);
- return result;
- }
-
- if (dats is null) return null;
-
- var envCell = dats.Get(cellId);
- if (envCell is null) return null;
-
- var fromDat = new (ushort, ushort)[envCell.CellPortals.Count];
- for (int i = 0; i < envCell.CellPortals.Count; i++)
- fromDat[i] = (envCell.CellPortals[i].OtherCellId, envCell.CellPortals[i].PolygonId);
- return fromDat;
- }
-
- private static void CollectExitPolygonsFromLoadedCell(LoadedCell cell, List sink)
- {
- 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);
- sink.Add(worldPoly);
- }
- }
-
- ///
- /// RR7.3 fallback: resolve a cell's exit portal polygons directly from the
- /// dat when the cell hasn't yet streamed into .
- /// Mirrors PortalService.GetPortalsForCell at
- /// WorldBuilder.Shared/Services/PortalService.cs:99-139.
- ///
- private static void CollectExitPolygonsFromDat(
- uint cellId,
- DatCollection dats,
- Vector3 landblockOrigin,
- List sink)
- {
- var envCell = dats.Get(cellId);
- if (envCell is null) return;
- if (envCell.EnvironmentId == 0) return;
-
- var environment = dats.Get(0x0D000000u | envCell.EnvironmentId);
- if (environment is null) return;
- if (!environment.Cells.TryGetValue(envCell.CellStructure, out var cellStruct)) return;
-
- var cellOriginWorld = envCell.Position.Origin + landblockOrigin;
- var cellTransform =
- Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation) *
- Matrix4x4.CreateTranslation(cellOriginWorld);
-
- foreach (var portal in envCell.CellPortals)
- {
- if (portal.OtherCellId != 0xFFFF) continue;
- if (!cellStruct.Polygons.TryGetValue(portal.PolygonId, out var poly)) continue;
- if (poly.VertexIds.Count < 3) continue;
-
- var localPoly = new Vector3[poly.VertexIds.Count];
- bool allFound = true;
- for (int v = 0; v < poly.VertexIds.Count; v++)
- {
- if (!cellStruct.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[v], out var vtx))
- {
- allFound = false;
- break;
- }
- localPoly[v] = vtx.Origin;
- }
- if (!allFound) continue;
-
- var worldPoly = new Vector3[localPoly.Length];
- for (int v = 0; v < localPoly.Length; v++)
- worldPoly[v] = Vector3.Transform(localPoly[v], cellTransform);
- sink.Add(worldPoly);
- }
- }
}