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)));
+ }
+}