From 5bc72d5cd114f8eef910b785f7bd55db91fb6d60 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 2 Jun 2026 09:07:18 +0200 Subject: [PATCH] =?UTF-8?q?feat(core):=20UCG=20Stage=201=20=E2=80=94=20Env?= =?UTF-8?q?Cell.FromDat=20derivation=20(mirrors=20BuildLoadedCell)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.Core/World/Cells/CellPortal.cs | 9 ++- src/AcDream.Core/World/Cells/EnvCell.cs | 55 ++++++++++++++++++- src/AcDream.Core/World/Cells/ObjCell.cs | 6 +- .../World/Cells/EnvCellFromDatTests.cs | 43 +++++++++++++++ 4 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 tests/AcDream.Core.Tests/World/Cells/EnvCellFromDatTests.cs diff --git a/src/AcDream.Core/World/Cells/CellPortal.cs b/src/AcDream.Core/World/Cells/CellPortal.cs index 456c5bd..c82c5f3 100644 --- a/src/AcDream.Core/World/Cells/CellPortal.cs +++ b/src/AcDream.Core/World/Cells/CellPortal.cs @@ -5,12 +5,17 @@ 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). +/// Unified cell-to-cell portal edge for Phase-U purposes. Superset of the three +/// legacy portal types (render CellPortalInfo, physics PortalInfo, +/// PortalPlane). ExactMatch (packed bit 0 of ) +/// is carried in but not yet exposed as a named property. /// Retail anchor: CCellPortal (acclient.h:32300). /// public readonly struct CellPortal { + /// + /// Full 32-bit (landblock-prefixed) cell id, unlike physics PortalInfo.OtherCellId which is low-16 only. + /// public uint OtherCellId { get; } public ushort OtherPortalId { get; } public ushort PolygonId { get; } diff --git a/src/AcDream.Core/World/Cells/EnvCell.cs b/src/AcDream.Core/World/Cells/EnvCell.cs index 3a256b1..f4dc2e0 100644 --- a/src/AcDream.Core/World/Cells/EnvCell.cs +++ b/src/AcDream.Core/World/Cells/EnvCell.cs @@ -1,7 +1,9 @@ +using System; using System.Collections.Generic; using System.Numerics; using AcDream.Core.Physics; // BSPQuery -using DatReaderWriter.Types; // CellBSPTree +using DatReaderWriter.Enums; // EnvCellFlags +using DatReaderWriter.Types; // CellBSPTree, CellStruct namespace AcDream.Core.World.Cells; @@ -30,4 +32,55 @@ public sealed class EnvCell : ObjCell && local.Y >= LocalBoundsMin.Y && local.Y <= LocalBoundsMax.Y && local.Z >= LocalBoundsMin.Z && local.Z <= LocalBoundsMax.Z; } + + /// + /// Build an EnvCell from dat data. Mirrors the render derivation in + /// BuildLoadedCell (GameWindow.cs:5588-5704) so Core and render stay equivalent. + /// MUST be the physics-verbatim transform + /// (no +2 cm render lift). + /// + public static EnvCell FromDat(uint id, DatReaderWriter.DBObjs.EnvCell datCell, + CellStruct cellStruct, Matrix4x4 worldTransform) + { + Matrix4x4.Invert(worldTransform, out var inverse); + + var min = new Vector3(float.MaxValue); + var max = new Vector3(float.MinValue); + foreach (var kvp in cellStruct.VertexArray.Vertices) + { + var p = new Vector3(kvp.Value.Origin.X, kvp.Value.Origin.Y, kvp.Value.Origin.Z); + min = Vector3.Min(min, p); + max = Vector3.Max(max, p); + } + if (min.X == float.MaxValue) { min = Vector3.Zero; max = Vector3.Zero; } + + var portals = new List(datCell.CellPortals.Count); + foreach (var p in datCell.CellPortals) + { + portals.Add(new CellPortal( + otherCellId: p.OtherCellId, + otherPortalId: p.OtherPortalId, + polygonId: p.PolygonId, + flags: (ushort)p.Flags, + polygonLocal: ResolvePortalPolygon(cellStruct, p.PolygonId))); + } + + uint lbPrefix = id & 0xFFFF0000u; + var stab = new List(datCell.VisibleCells.Count); + foreach (var low in datCell.VisibleCells) stab.Add(lbPrefix | low); + bool seenOutside = datCell.Flags.HasFlag(EnvCellFlags.SeenOutside); + + return new EnvCell(id, worldTransform, inverse, min, max, portals, stab, + seenOutside, cellStruct.CellBSP); + } + + private static IReadOnlyList ResolvePortalPolygon(CellStruct cellStruct, ushort polygonId) + { + if (!cellStruct.Polygons.TryGetValue(polygonId, out var poly)) return Array.Empty(); + var verts = new List(poly.VertexIds.Count); + foreach (var vid in poly.VertexIds) + if (cellStruct.VertexArray.Vertices.TryGetValue((ushort)vid, out var v)) + verts.Add(new Vector3(v.Origin.X, v.Origin.Y, v.Origin.Z)); + return verts; + } } diff --git a/src/AcDream.Core/World/Cells/ObjCell.cs b/src/AcDream.Core/World/Cells/ObjCell.cs index 7fdb619..9c8f0bb 100644 --- a/src/AcDream.Core/World/Cells/ObjCell.cs +++ b/src/AcDream.Core/World/Cells/ObjCell.cs @@ -20,7 +20,11 @@ public abstract class ObjCell public IReadOnlyList StabList { get; } public bool SeenOutside { get; } - /// Retail magnitude dispatch (CObjCell::GetVisible, pseudo_c:308215). + /// + /// Retail magnitude dispatch (CObjCell::GetVisible, pseudo_c:308215). + /// Note: retail's CObjCell::GetVisible tests the full id; every real prefixed EnvCell id is >= 0x100. + /// This masks the low-16 so it works for both bare-16 and landblock-prefixed ids. + /// public bool IsEnv => (Id & 0xFFFFu) >= 0x100u; protected ObjCell(uint id, Matrix4x4 worldTransform, Matrix4x4 inverseWorldTransform, diff --git a/tests/AcDream.Core.Tests/World/Cells/EnvCellFromDatTests.cs b/tests/AcDream.Core.Tests/World/Cells/EnvCellFromDatTests.cs new file mode 100644 index 0000000..b13d5c7 --- /dev/null +++ b/tests/AcDream.Core.Tests/World/Cells/EnvCellFromDatTests.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Numerics; +using AcDream.Core.World.Cells; +using DatReaderWriter.Enums; +using DatReaderWriter.Types; +using Xunit; +using DatEnvCell = DatReaderWriter.DBObjs.EnvCell; +using DatCellPortal = DatReaderWriter.Types.CellPortal; + +namespace AcDream.Core.Tests.World.Cells; + +public class EnvCellFromDatTests +{ + [Fact] + public void FromDat_DerivesSeenOutside_OtherPortalId_PrefixedStab_AndBoundsFallback() + { + var cellStruct = new CellStruct + { + VertexArray = new VertexArray { Vertices = new Dictionary() }, // empty -> bounds fallback + Polygons = new Dictionary(), + }; // CellBSP defaults to null → ContainmentBsp will be null + var dat = new DatEnvCell + { + Flags = EnvCellFlags.SeenOutside, + CellPortals = new List + { + new() { OtherCellId = 0x0105, PolygonId = 0, OtherPortalId = 7, Flags = (PortalFlags)0 }, + }, + VisibleCells = new List { 0x0105, 0x0106 }, + }; + + var env = EnvCell.FromDat(0xA9B40104u, dat, cellStruct, Matrix4x4.Identity); + + Assert.True(env.SeenOutside); + Assert.Single(env.Portals); + Assert.Equal(0x0105u, env.Portals[0].OtherCellId); + Assert.Equal((ushort)7, env.Portals[0].OtherPortalId); + Assert.Equal(new[] { 0xA9B40105u, 0xA9B40106u }, env.StabList); + Assert.Equal(Vector3.Zero, env.LocalBoundsMin); + Assert.Equal(Vector3.Zero, env.LocalBoundsMax); + Assert.Null(env.ContainmentBsp); + } +}