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