From 9cb15710bee190640b627dc5c7b9bc43dd6e30f2 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 2 Jun 2026 08:51:55 +0200 Subject: [PATCH] =?UTF-8?q?feat(core):=20UCG=20Stage=201=20=E2=80=94=20Obj?= =?UTF-8?q?Cell=20base=20+=20CellPortal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces AcDream.Core.World.Cells namespace with the two foundational types for the Unified Cell Graph. CellPortal is a readonly struct unifying the three legacy portal representations; ObjCell is the abstract base for all traversable cells with the retail id-magnitude IsEnv discriminator (CObjCell::GetVisible, pseudo_c:308215). Zero consumers; zero behavior change. 5/5 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.Core/World/Cells/CellPortal.cs | 32 ++++++++++++++ src/AcDream.Core/World/Cells/ObjCell.cs | 43 +++++++++++++++++++ .../World/Cells/ObjCellBaseTests.cs | 36 ++++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 src/AcDream.Core/World/Cells/CellPortal.cs create mode 100644 src/AcDream.Core/World/Cells/ObjCell.cs create mode 100644 tests/AcDream.Core.Tests/World/Cells/ObjCellBaseTests.cs diff --git a/src/AcDream.Core/World/Cells/CellPortal.cs b/src/AcDream.Core/World/Cells/CellPortal.cs new file mode 100644 index 0000000..456c5bd --- /dev/null +++ b/src/AcDream.Core/World/Cells/CellPortal.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace AcDream.Core.World.Cells; + +/// +/// Unified cell-to-cell portal edge. Superset of the three legacy portal types +/// (render CellPortalInfo, physics PortalInfo, PortalPlane). +/// Retail anchor: CCellPortal (acclient.h:32300). +/// +public readonly struct CellPortal +{ + public uint OtherCellId { get; } + public ushort OtherPortalId { get; } + public ushort PolygonId { get; } + public ushort Flags { get; } + /// Matches the physics PortalInfo.PortalSide convention (PortalInfo.cs:44). + public bool PortalSide => (Flags & 0x2) == 0; + /// Cell-local portal polygon vertices. Carried now; consumed by PView at Stage 3. + public IReadOnlyList PolygonLocal { get; } + + public CellPortal(uint otherCellId, ushort otherPortalId, ushort polygonId, ushort flags, + IReadOnlyList? polygonLocal = null) + { + OtherCellId = otherCellId; + OtherPortalId = otherPortalId; + PolygonId = polygonId; + Flags = flags; + PolygonLocal = polygonLocal ?? Array.Empty(); + } +} diff --git a/src/AcDream.Core/World/Cells/ObjCell.cs b/src/AcDream.Core/World/Cells/ObjCell.cs new file mode 100644 index 0000000..7fdb619 --- /dev/null +++ b/src/AcDream.Core/World/Cells/ObjCell.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Numerics; + +namespace AcDream.Core.World.Cells; + +/// +/// Base for every cell the player can stand in. Retail anchor: CObjCell +/// (acclient.h:30915). The id magnitude is the type discriminator +/// (): low-16 >= 0x100 => indoor , +/// else outdoor . +/// +public abstract class ObjCell +{ + public uint Id { get; } + public Matrix4x4 WorldTransform { get; } + public Matrix4x4 InverseWorldTransform { get; } + public Vector3 LocalBoundsMin { get; } + public Vector3 LocalBoundsMax { get; } + public IReadOnlyList Portals { get; } + public IReadOnlyList StabList { get; } + public bool SeenOutside { get; } + + /// Retail magnitude dispatch (CObjCell::GetVisible, pseudo_c:308215). + public bool IsEnv => (Id & 0xFFFFu) >= 0x100u; + + protected ObjCell(uint id, Matrix4x4 worldTransform, Matrix4x4 inverseWorldTransform, + Vector3 localBoundsMin, Vector3 localBoundsMax, + IReadOnlyList portals, IReadOnlyList stabList, + bool seenOutside) + { + Id = id; + WorldTransform = worldTransform; + InverseWorldTransform = inverseWorldTransform; + LocalBoundsMin = localBoundsMin; + LocalBoundsMax = localBoundsMax; + Portals = portals; + StabList = stabList; + SeenOutside = seenOutside; + } + + /// Retail CObjCell::point_in_cell (vtable +0x84). Is a world point inside this cell? + public abstract bool PointInCell(Vector3 worldPoint); +} diff --git a/tests/AcDream.Core.Tests/World/Cells/ObjCellBaseTests.cs b/tests/AcDream.Core.Tests/World/Cells/ObjCellBaseTests.cs new file mode 100644 index 0000000..8dbb5a2 --- /dev/null +++ b/tests/AcDream.Core.Tests/World/Cells/ObjCellBaseTests.cs @@ -0,0 +1,36 @@ +using System.Numerics; +using AcDream.Core.World.Cells; +using Xunit; + +namespace AcDream.Core.Tests.World.Cells; + +public class ObjCellBaseTests +{ + private sealed class StubCell : ObjCell + { + public StubCell(uint id) + : base(id, Matrix4x4.Identity, Matrix4x4.Identity, + Vector3.Zero, Vector3.One, + System.Array.Empty(), System.Array.Empty(), false) { } + public override bool PointInCell(Vector3 worldPoint) => false; + } + + [Theory] + [InlineData(0xA9B40174u, true)] + [InlineData(0xA9B40005u, false)] + [InlineData(0xA9B40100u, true)] + [InlineData(0xA9B400FFu, false)] + public void IsEnv_DispatchesByLow16Magnitude(uint id, bool expected) + => Assert.Equal(expected, new StubCell(id).IsEnv); + + [Fact] + public void Ctor_StoresBaseProperties() + { + var c = new StubCell(0xA9B40174u); + Assert.Equal(0xA9B40174u, c.Id); + Assert.Equal(Vector3.One, c.LocalBoundsMax); + Assert.Empty(c.Portals); + Assert.Empty(c.StabList); + Assert.False(c.SeenOutside); + } +}