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