using System; using System.Collections.Generic; using System.Numerics; using AcDream.Core.Physics; // BSPQuery using DatReaderWriter.Enums; // EnvCellFlags using DatReaderWriter.Types; // CellBSPTree, CellStruct 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; } /// /// 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(); if (datCell.VisibleCells is not null) // match BuildLoadedCell:5701 null guard 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) || poly.VertexIds.Count < 3) return Array.Empty(); var verts = new Vector3[poly.VertexIds.Count]; for (int i = 0; i < poly.VertexIds.Count; i++) { if (!cellStruct.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[i], out var v)) return Array.Empty(); // any miss -> empty (matches BuildLoadedCell:5686-5691) verts[i] = new Vector3(v.Origin.X, v.Origin.Y, v.Origin.Z); } return verts; } }