From 76c9e2f07d2f3fde265cc6f0452acdf73d4f5b46 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 2 Jun 2026 08:59:52 +0200 Subject: [PATCH] =?UTF-8?q?feat(core):=20UCG=20Stage=201=20=E2=80=94=20Env?= =?UTF-8?q?Cell=20+=20PointInCell=20(AABB/BSP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `EnvCell` (sealed, extends `ObjCell`) with a primitive constructor and `PointInCell` that uses the cell-containment BSP when present, else falls back to an AABB test. Retail anchor: CEnvCell (acclient.h:32072). BSP branch delegates to `BSPQuery.PointInsideCellBsp` (BSPQuery.cs:1034); the AABB branch is the genuinely new logic. No `FromDat` factory — that is a separate later task. Consumed by nobody yet (Stage 1 scaffold). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.Core/World/Cells/EnvCell.cs | 33 +++++++++++++++++++ .../World/Cells/EnvCellTests.cs | 33 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 src/AcDream.Core/World/Cells/EnvCell.cs create mode 100644 tests/AcDream.Core.Tests/World/Cells/EnvCellTests.cs diff --git a/src/AcDream.Core/World/Cells/EnvCell.cs b/src/AcDream.Core/World/Cells/EnvCell.cs new file mode 100644 index 0000000..3a256b1 --- /dev/null +++ b/src/AcDream.Core/World/Cells/EnvCell.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Numerics; +using AcDream.Core.Physics; // BSPQuery +using DatReaderWriter.Types; // CellBSPTree + +namespace AcDream.Core.World.Cells; + +/// Indoor room cell. Retail anchor: CEnvCell (acclient.h:32072). +public sealed class EnvCell : ObjCell +{ + /// Cell-containment BSP (retail CellStruct.CellBSP). Null => AABB fallback. + public CellBSPTree? ContainmentBsp { get; } + + public EnvCell(uint id, Matrix4x4 worldTransform, Matrix4x4 inverseWorldTransform, + Vector3 localBoundsMin, Vector3 localBoundsMax, + IReadOnlyList portals, IReadOnlyList stabList, + bool seenOutside, CellBSPTree? containmentBsp) + : base(id, worldTransform, inverseWorldTransform, localBoundsMin, localBoundsMax, + portals, stabList, seenOutside) + { + ContainmentBsp = containmentBsp; + } + + public override bool PointInCell(Vector3 worldPoint) + { + var local = Vector3.Transform(worldPoint, InverseWorldTransform); + if (ContainmentBsp?.Root is not null) + return BSPQuery.PointInsideCellBsp(ContainmentBsp.Root, local); // BSPQuery.cs:1034 + return local.X >= LocalBoundsMin.X && local.X <= LocalBoundsMax.X + && local.Y >= LocalBoundsMin.Y && local.Y <= LocalBoundsMax.Y + && local.Z >= LocalBoundsMin.Z && local.Z <= LocalBoundsMax.Z; + } +} diff --git a/tests/AcDream.Core.Tests/World/Cells/EnvCellTests.cs b/tests/AcDream.Core.Tests/World/Cells/EnvCellTests.cs new file mode 100644 index 0000000..8dd2408 --- /dev/null +++ b/tests/AcDream.Core.Tests/World/Cells/EnvCellTests.cs @@ -0,0 +1,33 @@ +using System.Numerics; +using AcDream.Core.World.Cells; +using Xunit; + +namespace AcDream.Core.Tests.World.Cells; + +public class EnvCellTests +{ + private static EnvCell Make(Vector3 min, Vector3 max, Matrix4x4? transform = null) + { + var t = transform ?? Matrix4x4.Identity; + Matrix4x4.Invert(t, out var inv); + return new EnvCell(0xA9B40174u, t, inv, min, max, + System.Array.Empty(), System.Array.Empty(), + seenOutside: false, containmentBsp: null); + } + + [Fact] + public void PointInCell_NullBsp_Aabb_InsideIsTrue() + => Assert.True(Make(new Vector3(0,0,0), new Vector3(10,10,10)).PointInCell(new Vector3(5,5,5))); + + [Fact] + public void PointInCell_NullBsp_Aabb_OutsideIsFalse() + => Assert.False(Make(new Vector3(0,0,0), new Vector3(10,10,10)).PointInCell(new Vector3(20,5,5))); + + [Fact] + public void PointInCell_TransformsWorldToLocalBeforeTesting() + { + var c = Make(new Vector3(0,0,0), new Vector3(10,10,10), Matrix4x4.CreateTranslation(100,0,0)); + Assert.True(c.PointInCell(new Vector3(105,5,5))); + Assert.False(c.PointInCell(new Vector3(5,5,5))); + } +}