using System.Collections.Immutable; using System.Numerics; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; using DatReaderWriter.Types; namespace AcDream.App.Rendering.Wb; /// /// One drawable EnvCell shell prepared by the streaming worker. This is CPU-only /// placement data; the render thread schedules mesh preparation when it commits /// the containing . /// public sealed record EnvCellShellPlacement( uint CellId, ulong GeometryId, uint EnvironmentId, ushort CellStructure, ImmutableArray Surfaces, Vector3 WorldPosition, Quaternion Rotation, Matrix4x4 Transform, WbBoundingBox LocalBounds, WbBoundingBox WorldBounds); /// /// Complete, immutable worker output for one landblock's indoor cells. It owns /// both portal-visibility cells and drawable shell placements so neither can be /// drained by, or mixed with, another streaming completion. /// public sealed class EnvCellLandblockBuild { public EnvCellLandblockBuild( uint landblockId, IEnumerable visibilityCells, IEnumerable shells) { LandblockId = landblockId; VisibilityCells = visibilityCells.ToImmutableArray(); Shells = shells.ToImmutableArray(); uint expectedPrefix = landblockId & 0xFFFF0000u; if (VisibilityCells.Any(cell => (cell.CellId & 0xFFFF0000u) != expectedPrefix)) throw new ArgumentException("A visibility cell belongs to a different landblock.", nameof(visibilityCells)); if (Shells.Any(shell => (shell.CellId & 0xFFFF0000u) != expectedPrefix)) throw new ArgumentException("A render shell belongs to a different landblock.", nameof(shells)); } public uint LandblockId { get; } public ImmutableArray VisibilityCells { get; } public ImmutableArray Shells { get; } } /// /// Transaction-local builder used only by one streaming job. Unlike the former /// global pending bags, instances of this class are never shared between jobs or /// observed by the render thread before returns. /// public sealed class EnvCellLandblockBuildBuilder { private readonly uint _landblockId; private readonly List _visibilityCells = new(); private readonly List _shells = new(); private bool _built; public EnvCellLandblockBuildBuilder(uint landblockId) { _landblockId = landblockId; } public void AddCell( uint envCellId, EnvCell envCell, CellStruct cellStruct, Vector3 physicsCellOrigin, Matrix4x4 physicsCellTransform, Vector3 shellWorldPosition, Matrix4x4 shellTransform, bool hasDrawableGeometry) { if (_built) throw new InvalidOperationException("This landblock cell build is already complete."); var localBounds = ComputeLocalBounds(cellStruct); _visibilityCells.Add(BuildVisibilityCell( envCellId, envCell, cellStruct, physicsCellOrigin, physicsCellTransform, localBounds)); if (!hasDrawableGeometry) return; ulong geometryId = ComputeGeometryId( envCell.EnvironmentId, envCell.CellStructure, envCell.Surfaces); _shells.Add(new EnvCellShellPlacement( envCellId, geometryId, envCell.EnvironmentId, envCell.CellStructure, envCell.Surfaces.ToImmutableArray(), shellWorldPosition, envCell.Position.Orientation, shellTransform, localBounds, TransformBoundingBox(localBounds, shellTransform))); } public EnvCellLandblockBuild Build() { if (_built) throw new InvalidOperationException("This landblock cell build is already complete."); _built = true; return new EnvCellLandblockBuild(_landblockId, _visibilityCells, _shells); } /// /// Source: WB EnvCellRenderManager.cs:94-103 (verbatim arithmetic). /// public static ulong ComputeGeometryId( uint environmentId, ushort cellStructure, IReadOnlyList surfaces) { var hash = 17L; hash = hash * 31 + (int)environmentId; hash = hash * 31 + cellStructure; foreach (var surface in surfaces) hash = hash * 31 + surface; return (ulong)hash | 0x2_0000_0000UL; } private static WbBoundingBox ComputeLocalBounds(CellStruct cellStruct) { var min = new Vector3(float.MaxValue); var max = new Vector3(float.MinValue); foreach (var vertex in cellStruct.VertexArray.Vertices.Values) { var position = new Vector3(vertex.Origin.X, vertex.Origin.Y, vertex.Origin.Z); min = Vector3.Min(min, position); max = Vector3.Max(max, position); } return min.X == float.MaxValue ? new WbBoundingBox(Vector3.Zero, Vector3.Zero) : new WbBoundingBox(min, max); } private static WbBoundingBox TransformBoundingBox(WbBoundingBox box, Matrix4x4 transform) { Span corners = stackalloc Vector3[8] { new(box.Min.X, box.Min.Y, box.Min.Z), new(box.Max.X, box.Min.Y, box.Min.Z), new(box.Min.X, box.Max.Y, box.Min.Z), new(box.Max.X, box.Max.Y, box.Min.Z), new(box.Min.X, box.Min.Y, box.Max.Z), new(box.Max.X, box.Min.Y, box.Max.Z), new(box.Min.X, box.Max.Y, box.Max.Z), new(box.Max.X, box.Max.Y, box.Max.Z), }; var min = new Vector3(float.MaxValue); var max = new Vector3(float.MinValue); foreach (var corner in corners) { var transformed = Vector3.Transform(corner, transform); min = Vector3.Min(min, transformed); max = Vector3.Max(max, transformed); } return new WbBoundingBox(min, max); } /// /// Mechanical extraction of the former GameWindow.BuildLoadedCell. Portal-side behavior /// is unchanged: retail PView::InitCell (0x005a4b70) uses the dat PortalSide /// bit, and the reciprocal portal index follows the named retail path at /// pseudo-C line 433557. /// private static LoadedCell BuildVisibilityCell( uint envCellId, EnvCell envCell, CellStruct cellStruct, Vector3 cellOrigin, Matrix4x4 cellTransform, WbBoundingBox localBounds) { Matrix4x4.Invert(cellTransform, out var inverse); var portals = new List(); var clipPlanes = new List(); var portalPolygons = new List(); foreach (var portal in envCell.CellPortals) { portals.Add(new CellPortalInfo( portal.OtherCellId, portal.PolygonId, (ushort)portal.Flags, portal.OtherPortalId)); if (cellStruct.Polygons.TryGetValue(portal.PolygonId, out var poly) && poly.VertexIds.Count >= 3) { Vector3 p0 = Vector3.Zero, p1 = Vector3.Zero, p2 = Vector3.Zero; bool found = true; if (cellStruct.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[0], out var v0)) p0 = new Vector3(v0.Origin.X, v0.Origin.Y, v0.Origin.Z); else found = false; if (found && cellStruct.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[1], out var v1)) p1 = new Vector3(v1.Origin.X, v1.Origin.Y, v1.Origin.Z); else found = false; if (found && cellStruct.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[2], out var v2)) p2 = new Vector3(v2.Origin.X, v2.Origin.Y, v2.Origin.Z); else found = false; if (found) { var normal = Vector3.Normalize(Vector3.Cross(p1 - p0, p2 - p0)); float d = -Vector3.Dot(normal, p0); int insideSide = ((ushort)portal.Flags & 0x2) == 0 ? 1 : 0; clipPlanes.Add(new PortalClipPlane { Normal = normal, D = d, InsideSide = insideSide, }); } else { clipPlanes.Add(default); } } else { clipPlanes.Add(default); } Vector3[] polygonVertices = Array.Empty(); if (cellStruct.Polygons.TryGetValue(portal.PolygonId, out var portalPolygon) && portalPolygon.VertexIds.Count >= 3) { polygonVertices = new Vector3[portalPolygon.VertexIds.Count]; bool allResolved = true; for (int vertexIndex = 0; vertexIndex < portalPolygon.VertexIds.Count; vertexIndex++) { if (cellStruct.VertexArray.Vertices.TryGetValue( (ushort)portalPolygon.VertexIds[vertexIndex], out var vertex)) { polygonVertices[vertexIndex] = new Vector3( vertex.Origin.X, vertex.Origin.Y, vertex.Origin.Z); } else { allResolved = false; break; } } if (!allResolved) polygonVertices = Array.Empty(); } portalPolygons.Add(polygonVertices); } uint landblockPrefix = envCellId & 0xFFFF0000u; var visibleCells = new List(); if (envCell.VisibleCells is not null) { foreach (var lowId in envCell.VisibleCells) visibleCells.Add(landblockPrefix | lowId); } return new LoadedCell { CellId = envCellId, WorldPosition = cellOrigin, WorldTransform = cellTransform, InverseWorldTransform = inverse, LocalBoundsMin = localBounds.Min, LocalBoundsMax = localBounds.Max, Portals = portals, ClipPlanes = clipPlanes, PortalPolygons = portalPolygons, VisibleCells = visibleCells, SeenOutside = envCell.Flags.HasFlag(EnvCellFlags.SeenOutside), }; } }