From 07c5981824f9081170762c1fb57cd6aab3af3592 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 27 May 2026 14:07:13 +0200 Subject: [PATCH] =?UTF-8?q?Revert=20"fix(render):=20Phase=20A8=20RR7.3=20?= =?UTF-8?q?=E2=80=94=20dat-driven=20BFS=20in=20BuildingLoader"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 56673e1b1ebef9b32fe3220b5a9ec306f3bc2bda. --- src/AcDream.App/Rendering/GameWindow.cs | 12 +- .../Rendering/Wb/BuildingLoader.cs | 189 +++++------------- 2 files changed, 47 insertions(+), 154 deletions(-) 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); - } - } }