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>
This commit is contained in:
parent
f44a9bf943
commit
f125fdb220
6 changed files with 470 additions and 0 deletions
57
src/AcDream.App/Rendering/Wb/Building.cs
Normal file
57
src/AcDream.App/Rendering/Wb/Building.cs
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace AcDream.App.Rendering.Wb;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase A8 (2026-05-26): a logical building — one or more EnvCells linked
|
||||||
|
/// via the dat-level <c>LandBlockInfo.Buildings</c> entry. Building shells (cottage
|
||||||
|
/// walls, inn walls — <c>IsBuildingShell=true</c> entities) render unconditionally
|
||||||
|
/// when the camera is inside this building's cells. The exit portal polygons
|
||||||
|
/// are stencil-marked so outdoor visibility leaks through portal silhouettes
|
||||||
|
/// only.
|
||||||
|
///
|
||||||
|
/// <para>Step 5 (cross-building visibility via 3-stencil-bit pipeline) uses
|
||||||
|
/// the occlusion-query state to skip rendering when the building's portals
|
||||||
|
/// weren't visible last frame.</para>
|
||||||
|
///
|
||||||
|
/// <para>WB reference: <c>WorldBuilder.Shared/Services/PortalService.cs</c>
|
||||||
|
/// (<c>BuildingPortalGroup</c>) and <c>PortalRenderManager.cs</c> step-5 lifecycle.
|
||||||
|
/// Retail reference: <c>docs/research/named-retail/acclient.h:32035</c>
|
||||||
|
/// (<c>BuildInfo</c>) + <c>32094</c> (<c>CBldPortal</c>).</para>
|
||||||
|
/// </summary>
|
||||||
|
public sealed class Building
|
||||||
|
{
|
||||||
|
/// <summary>Unique within a landblock; allocated sequentially by <see cref="BuildingLoader"/>
|
||||||
|
/// starting at 1 (0 is reserved for "no building" semantics on <c>LoadedCell</c>).</summary>
|
||||||
|
public required uint BuildingId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>The EnvCells this building owns. Includes all cells reachable
|
||||||
|
/// from the building's entry portals via interior portals (no exit portals).
|
||||||
|
/// Populated by <see cref="BuildingLoader.Build"/> via BFS over
|
||||||
|
/// <see cref="AcDream.App.Rendering.LoadedCell.Portals"/>.</summary>
|
||||||
|
public required HashSet<uint> EnvCellIds { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Exit portal polygons in world space (each polygon is a triangle
|
||||||
|
/// fan from vertex 0). Stencil-marked + far-depth-punched at Steps 1+2 of
|
||||||
|
/// WB's RenderInsideOut pipeline (RR7). Collected during
|
||||||
|
/// <see cref="BuildingLoader.Build"/> Step C by transforming cell-local portal
|
||||||
|
/// polygon vertices via <see cref="AcDream.App.Rendering.LoadedCell.WorldTransform"/>.</summary>
|
||||||
|
public required IReadOnlyList<Vector3[]> ExitPortalPolygons { get; init; }
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Step 5 occlusion-query state (mutable, per-frame, RR9 scope).
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>GL query object handle; lazily created on first use by the
|
||||||
|
/// Step 5 occlusion-query pass (RR9). 0 = not yet created.</summary>
|
||||||
|
public uint QueryId;
|
||||||
|
|
||||||
|
/// <summary>True after the first <c>BeginQuery</c> call; controls whether the
|
||||||
|
/// read-back path is safe to execute on the next frame.</summary>
|
||||||
|
public bool QueryStarted;
|
||||||
|
|
||||||
|
/// <summary>Previous-frame query result. When false, the building's interior
|
||||||
|
/// render is skipped (Step 5 early-out in RR9 + RR11).</summary>
|
||||||
|
public bool WasVisible;
|
||||||
|
}
|
||||||
138
src/AcDream.App/Rendering/Wb/BuildingLoader.cs
Normal file
138
src/AcDream.App/Rendering/Wb/BuildingLoader.cs
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.App.Rendering;
|
||||||
|
using DatReaderWriter.DBObjs;
|
||||||
|
|
||||||
|
namespace AcDream.App.Rendering.Wb;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase A8 (2026-05-26): static factory that builds a per-landblock
|
||||||
|
/// <see cref="BuildingRegistry"/> from a <see cref="LandBlockInfo"/>'s
|
||||||
|
/// <c>Buildings</c> array.
|
||||||
|
///
|
||||||
|
/// <para>Algorithm (mirrors WB's <c>PortalService.GetPortalsByBuilding</c> at
|
||||||
|
/// <c>WorldBuilder.Shared/Services/PortalService.cs:43-97</c>):</para>
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>Step A — seed the cell set from <c>BuildingInfo.Portals</c> entry portals.</item>
|
||||||
|
/// <item>Step B — BFS through <see cref="LoadedCell.Portals"/> to discover all
|
||||||
|
/// interior cells reachable from the entry portals (interior portals only;
|
||||||
|
/// exit portals — <c>OtherCellId == 0xFFFF</c> — terminate each BFS branch).</item>
|
||||||
|
/// <item>Step C — collect exit portal polygons in world space for the stencil
|
||||||
|
/// pipeline (Phase A8 Steps 1+2, RR7 scope).</item>
|
||||||
|
/// </list>
|
||||||
|
///
|
||||||
|
/// <para>Cells whose <c>LoadedCell</c> entries are missing from
|
||||||
|
/// <paramref name="cellsByCellId"/> are silently skipped (BFS bails at the
|
||||||
|
/// unloaded cell). In production, streaming loads all cells for a landblock
|
||||||
|
/// before <see cref="Build"/> runs, so the dict is always complete.</para>
|
||||||
|
///
|
||||||
|
/// <para><c>LoadedCell.BuildingId</c> stamping is wired in RR4, not here.</para>
|
||||||
|
///
|
||||||
|
/// <para>Retail references:
|
||||||
|
/// <c>docs/research/named-retail/acclient.h:32035</c> (<c>BuildInfo</c>) and
|
||||||
|
/// <c>:32094</c> (<c>CBldPortal</c>).</para>
|
||||||
|
/// </summary>
|
||||||
|
public static class BuildingLoader
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a <see cref="BuildingRegistry"/> from the supplied landblock data.
|
||||||
|
/// Building IDs are allocated sequentially starting at 1 (0 is reserved for
|
||||||
|
/// "no building" semantics used by <c>LoadedCell.BuildingId</c> in RR4).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="info">The dat-loaded <see cref="LandBlockInfo"/> for this landblock.</param>
|
||||||
|
/// <param name="landblockId">The 32-bit landblock id (e.g. <c>0xA9B40000</c>).
|
||||||
|
/// The high 16 bits are ORed with each 16-bit <c>OtherCellId</c> to produce
|
||||||
|
/// the full cell id.</param>
|
||||||
|
/// <param name="cellsByCellId">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.</param>
|
||||||
|
/// <returns>A fully populated <see cref="BuildingRegistry"/>; never null.</returns>
|
||||||
|
public static BuildingRegistry Build(
|
||||||
|
LandBlockInfo info,
|
||||||
|
uint landblockId,
|
||||||
|
IReadOnlyDictionary<uint, LoadedCell> 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<uint>();
|
||||||
|
var exitPortalPolys = new List<Vector3[]>();
|
||||||
|
|
||||||
|
// 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<uint>(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);
|
||||||
|
|
||||||
|
// NOTE: LoadedCell.BuildingId stamping is wired in RR4 (requires
|
||||||
|
// an internal setter on LoadedCell that doesn't exist yet). This
|
||||||
|
// comment is the placeholder called out in the plan's RR3-S11.
|
||||||
|
}
|
||||||
|
|
||||||
|
return reg;
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/AcDream.App/Rendering/Wb/BuildingRegistry.cs
Normal file
73
src/AcDream.App/Rendering/Wb/BuildingRegistry.cs
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
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<Building></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;
|
||||||
|
}
|
||||||
88
tests/AcDream.App.Tests/Rendering/Wb/BuildingLoaderTests.cs
Normal file
88
tests/AcDream.App.Tests/Rendering/Wb/BuildingLoaderTests.cs
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.App.Rendering.Wb;
|
||||||
|
using DatReaderWriter.DBObjs;
|
||||||
|
using DatReaderWriter.Types;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Rendering.Wb;
|
||||||
|
|
||||||
|
public class BuildingLoaderTests
|
||||||
|
{
|
||||||
|
// Helper: build a minimal LandBlockInfo with one BuildingInfo per supplied tuple.
|
||||||
|
// OtherCellId values are 16-bit cell-local ids (low word of full cell id).
|
||||||
|
private static LandBlockInfo MakeInfo(params (uint modelId, uint[] portalOtherCellIds)[] buildings)
|
||||||
|
{
|
||||||
|
var bls = new List<BuildingInfo>();
|
||||||
|
foreach (var (modelId, portals) in buildings)
|
||||||
|
{
|
||||||
|
var portalList = new List<BuildingPortal>();
|
||||||
|
foreach (var ocid in portals)
|
||||||
|
{
|
||||||
|
portalList.Add(new BuildingPortal
|
||||||
|
{
|
||||||
|
OtherCellId = (ushort)(ocid & 0xFFFFu),
|
||||||
|
Flags = 0,
|
||||||
|
OtherPortalId = 0,
|
||||||
|
StabList = new List<ushort>(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
bls.Add(new BuildingInfo
|
||||||
|
{
|
||||||
|
ModelId = modelId,
|
||||||
|
Frame = new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity },
|
||||||
|
Portals = portalList,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return new LandBlockInfo
|
||||||
|
{
|
||||||
|
Objects = new List<Stab>(),
|
||||||
|
Buildings = bls,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Empty_NoBuildings_EmptyRegistry()
|
||||||
|
{
|
||||||
|
var info = new LandBlockInfo { Objects = new List<Stab>(), Buildings = new List<BuildingInfo>() };
|
||||||
|
var reg = BuildingLoader.Build(info, landblockId: 0xA9B40000u, cellsByCellId: new Dictionary<uint, AcDream.App.Rendering.LoadedCell>());
|
||||||
|
Assert.Equal(0, reg.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OneBuilding_OnePortal_MapsToOneCell()
|
||||||
|
{
|
||||||
|
// Building points to cell 0x0150 in landblock 0xA9B40000 → full cell id 0xA9B40150
|
||||||
|
var info = MakeInfo((modelId: 0x02000123u, portalOtherCellIds: new[] { 0x0150u }));
|
||||||
|
// Pass an empty cell dict — loader seeds from BuildingInfo.Portals only
|
||||||
|
var reg = BuildingLoader.Build(info, landblockId: 0xA9B40000u, cellsByCellId: new Dictionary<uint, AcDream.App.Rendering.LoadedCell>());
|
||||||
|
Assert.Equal(1, reg.Count);
|
||||||
|
var building = System.Linq.Enumerable.First(reg.All());
|
||||||
|
Assert.Contains(0xA9B40150u, building.EnvCellIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OneBuilding_MultiplePortals_MapsToMultipleCells()
|
||||||
|
{
|
||||||
|
var info = MakeInfo((0x02000123u, new[] { 0x0150u, 0x0151u, 0x0152u }));
|
||||||
|
var reg = BuildingLoader.Build(info, 0xA9B40000u, new Dictionary<uint, AcDream.App.Rendering.LoadedCell>());
|
||||||
|
var building = System.Linq.Enumerable.First(reg.All());
|
||||||
|
Assert.Equal(3, building.EnvCellIds.Count);
|
||||||
|
Assert.Contains(0xA9B40150u, building.EnvCellIds);
|
||||||
|
Assert.Contains(0xA9B40151u, building.EnvCellIds);
|
||||||
|
Assert.Contains(0xA9B40152u, building.EnvCellIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TwoBuildings_AllocateSequentialIds()
|
||||||
|
{
|
||||||
|
var info = MakeInfo(
|
||||||
|
(0x02000001u, new[] { 0x0150u }),
|
||||||
|
(0x02000002u, new[] { 0x0160u }));
|
||||||
|
var reg = BuildingLoader.Build(info, 0xA9B40000u, new Dictionary<uint, AcDream.App.Rendering.LoadedCell>());
|
||||||
|
Assert.Equal(2, reg.Count);
|
||||||
|
var ids = new SortedSet<uint>();
|
||||||
|
foreach (var b in reg.All()) ids.Add(b.BuildingId);
|
||||||
|
Assert.Equal(new SortedSet<uint> { 1, 2 }, ids); // sequential 1, 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.App.Rendering.Wb;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Rendering.Wb;
|
||||||
|
|
||||||
|
public class BuildingRegistryTests
|
||||||
|
{
|
||||||
|
private static Building B(uint id, params uint[] cellIds) => new()
|
||||||
|
{
|
||||||
|
BuildingId = id,
|
||||||
|
EnvCellIds = new HashSet<uint>(cellIds),
|
||||||
|
ExitPortalPolygons = new List<Vector3[]>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Empty_NoBuildingsRegistered()
|
||||||
|
{
|
||||||
|
var reg = new BuildingRegistry();
|
||||||
|
Assert.Equal(0, reg.Count);
|
||||||
|
Assert.Empty(reg.All());
|
||||||
|
Assert.Empty(reg.GetBuildingsContainingCell(0xA9B40150u));
|
||||||
|
Assert.Null(reg.GetById(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Add_IndexesBothDirections()
|
||||||
|
{
|
||||||
|
var reg = new BuildingRegistry();
|
||||||
|
var b = B(1, 0xA9B40150u, 0xA9B40151u);
|
||||||
|
reg.Add(b);
|
||||||
|
|
||||||
|
Assert.Equal(1, reg.Count);
|
||||||
|
Assert.Same(b, reg.GetById(1));
|
||||||
|
Assert.Single(reg.GetBuildingsContainingCell(0xA9B40150u));
|
||||||
|
Assert.Single(reg.GetBuildingsContainingCell(0xA9B40151u));
|
||||||
|
Assert.Same(b, reg.GetBuildingsContainingCell(0xA9B40150u)[0]);
|
||||||
|
Assert.Empty(reg.GetBuildingsContainingCell(0xDEADBEEFu));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CellSharedBetweenTwoBuildings_GetBuildingsContainingCellReturnsBoth()
|
||||||
|
{
|
||||||
|
var reg = new BuildingRegistry();
|
||||||
|
var b1 = B(1, 0xA9B40150u, 0xA9B40151u);
|
||||||
|
var b2 = B(2, 0xA9B40151u, 0xA9B40152u); // shares 0151 with b1
|
||||||
|
reg.Add(b1);
|
||||||
|
reg.Add(b2);
|
||||||
|
|
||||||
|
var bothAt0151 = reg.GetBuildingsContainingCell(0xA9B40151u);
|
||||||
|
Assert.Equal(2, bothAt0151.Count);
|
||||||
|
Assert.Contains(b1, bothAt0151);
|
||||||
|
Assert.Contains(b2, bothAt0151);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void All_EnumeratesEveryBuilding()
|
||||||
|
{
|
||||||
|
var reg = new BuildingRegistry();
|
||||||
|
reg.Add(B(1, 0xA9B40150u));
|
||||||
|
reg.Add(B(2, 0xA9B40160u));
|
||||||
|
reg.Add(B(3, 0xA9B40170u));
|
||||||
|
|
||||||
|
var ids = new HashSet<uint>();
|
||||||
|
foreach (var b in reg.All()) ids.Add(b.BuildingId);
|
||||||
|
|
||||||
|
Assert.Equal(new HashSet<uint> { 1, 2, 3 }, ids);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
tests/AcDream.App.Tests/Rendering/Wb/BuildingTests.cs
Normal file
44
tests/AcDream.App.Tests/Rendering/Wb/BuildingTests.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.App.Rendering.Wb;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Rendering.Wb;
|
||||||
|
|
||||||
|
public class BuildingTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Building_RequiredFields_PopulateCorrectly()
|
||||||
|
{
|
||||||
|
var b = new Building
|
||||||
|
{
|
||||||
|
BuildingId = 42,
|
||||||
|
EnvCellIds = new HashSet<uint> { 0xA9B40150u, 0xA9B40151u },
|
||||||
|
ExitPortalPolygons = new List<Vector3[]>
|
||||||
|
{
|
||||||
|
new[] { new Vector3(0, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 1, 0) },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(42u, b.BuildingId);
|
||||||
|
Assert.Equal(2, b.EnvCellIds.Count);
|
||||||
|
Assert.Contains(0xA9B40150u, b.EnvCellIds);
|
||||||
|
Assert.Single(b.ExitPortalPolygons);
|
||||||
|
Assert.Equal(3, b.ExitPortalPolygons[0].Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Building_OcclusionQueryState_DefaultsZero()
|
||||||
|
{
|
||||||
|
var b = new Building
|
||||||
|
{
|
||||||
|
BuildingId = 0,
|
||||||
|
EnvCellIds = new HashSet<uint>(),
|
||||||
|
ExitPortalPolygons = new List<Vector3[]>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(0u, b.QueryId);
|
||||||
|
Assert.False(b.QueryStarted);
|
||||||
|
Assert.False(b.WasVisible);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue