using System.Collections.Immutable;
using System.Numerics;
using DatReaderWriter.Enums;
namespace AcDream.Core.Physics;
///
/// A checked half-open range into one immutable flat-asset array.
///
public readonly record struct FlatIndexRange(int Start, int Count)
{
public int EndExclusive => checked(Start + Count);
internal void Validate(int length, string name)
{
if (Start < 0 || Count < 0 || Start > length - Count)
{
throw new InvalidDataException(
$"{name} range [{Start}, {Start}+{Count}) exceeds array length {length}.");
}
}
}
///
/// Verbatim DAT sphere value. No normalization or coordinate conversion is
/// performed while preparing this record.
///
public readonly record struct FlatCollisionSphere(Vector3 Origin, float Radius);
///
/// Verbatim DAT cylinder-sphere value.
///
public readonly record struct FlatCollisionCylinder(
Vector3 Origin,
float Radius,
float Height);
///
/// One polygon row addressed through .
///
public readonly record struct FlatCollisionPolygon(
ushort Id,
Plane Plane,
CullMode SidesType,
int NumPoints,
FlatIndexRange VertexRange);
///
/// Immutable polygon rows plus their contiguous, source-ordered vertex stream.
/// Rows are strictly ordered by polygon ID so package output does not depend on
/// dictionary insertion or bucket order.
///
public sealed class FlatPolygonTable
{
public static FlatPolygonTable Empty { get; } = new(
ImmutableArray.Empty,
ImmutableArray.Empty);
public FlatPolygonTable(
ImmutableArray polygons,
ImmutableArray vertices)
{
if (polygons.IsDefault)
throw new ArgumentException("Polygon rows must not be default.", nameof(polygons));
if (vertices.IsDefault)
throw new ArgumentException("Vertex rows must not be default.", nameof(vertices));
ushort previousId = 0;
for (int i = 0; i < polygons.Length; i++)
{
FlatCollisionPolygon polygon = polygons[i];
polygon.VertexRange.Validate(vertices.Length, $"polygon[{i}].vertices");
if (polygon.NumPoints != polygon.VertexRange.Count)
{
throw new InvalidDataException(
$"polygon[{i}] NumPoints {polygon.NumPoints} does not match " +
$"its vertex count {polygon.VertexRange.Count}.");
}
if (!Enum.IsDefined(polygon.SidesType))
{
throw new InvalidDataException(
$"polygon[{i}] has unknown cull mode {(int)polygon.SidesType}.");
}
if (i != 0 && polygon.Id <= previousId)
{
throw new InvalidDataException(
"Flat polygon rows must be strictly ordered by unique ID.");
}
previousId = polygon.Id;
}
Polygons = polygons;
Vertices = vertices;
}
public ImmutableArray Polygons { get; }
public ImmutableArray Vertices { get; }
public bool TryFindPolygonIndex(ushort id, out int index)
{
int low = 0;
int high = Polygons.Length - 1;
while (low <= high)
{
int middle = low + ((high - low) >> 1);
ushort candidate = Polygons[middle].Id;
if (candidate == id)
{
index = middle;
return true;
}
if (candidate < id)
low = middle + 1;
else
high = middle - 1;
}
index = -1;
return false;
}
}
///
/// One physics BSP node. Child index -1 means absent. Polygon indices
/// address through
/// .
///
public readonly record struct FlatPhysicsBspNode(
BSPNodeType Type,
Plane SplittingPlane,
int PositiveChildIndex,
int NegativeChildIndex,
int LeafIndex,
int Solid,
FlatCollisionSphere BoundingSphere,
FlatIndexRange PolygonIndexRange);
///
/// Immutable, deterministic pre-order representation of a retail physics BSP.
/// It changes source storage only; traversal order and behavior remain the
/// retail port pinned by the graph differential oracle.
///
public sealed class FlatPhysicsBsp
{
public FlatPhysicsBsp(
int rootIndex,
ImmutableArray nodes,
ImmutableArray polygonIndexStream,
FlatPolygonTable polygonTable)
{
if (nodes.IsDefault)
throw new ArgumentException("Node rows must not be default.", nameof(nodes));
if (polygonIndexStream.IsDefault)
{
throw new ArgumentException(
"Polygon-index rows must not be default.",
nameof(polygonIndexStream));
}
ArgumentNullException.ThrowIfNull(polygonTable);
ValidateTree(rootIndex, nodes, polygonIndexStream, polygonTable.Polygons.Length);
RootIndex = rootIndex;
Nodes = nodes;
PolygonIndexStream = polygonIndexStream;
PolygonTable = polygonTable;
}
public int RootIndex { get; }
public ImmutableArray Nodes { get; }
public ImmutableArray PolygonIndexStream { get; }
public FlatPolygonTable PolygonTable { get; }
private static void ValidateTree(
int rootIndex,
ImmutableArray nodes,
ImmutableArray polygonIndexStream,
int polygonCount)
{
if (nodes.Length == 0)
{
if (rootIndex != -1)
throw new InvalidDataException("An empty physics BSP must use root index -1.");
if (polygonIndexStream.Length != 0)
{
throw new InvalidDataException(
"An empty physics BSP cannot contain leaf polygon indices.");
}
return;
}
if (rootIndex != 0)
{
throw new InvalidDataException(
$"A deterministic pre-order physics BSP must start at index 0, not {rootIndex}.");
}
for (int i = 0; i < nodes.Length; i++)
{
FlatPhysicsBspNode node = nodes[i];
FlatBspNodeContract.Validate(
node.Type,
node.PositiveChildIndex >= 0,
node.NegativeChildIndex >= 0,
$"physics node[{i}]");
ValidateChild(node.PositiveChildIndex, nodes.Length, i, "positive");
ValidateChild(node.NegativeChildIndex, nodes.Length, i, "negative");
node.PolygonIndexRange.Validate(
polygonIndexStream.Length,
$"physics node[{i}].polygonIndices");
}
for (int i = 0; i < polygonIndexStream.Length; i++)
{
int polygonIndex = polygonIndexStream[i];
if ((uint)polygonIndex >= (uint)polygonCount)
{
throw new InvalidDataException(
$"leaf polygon stream[{i}] index {polygonIndex} exceeds " +
$"polygon count {polygonCount}.");
}
}
ValidatePreOrderShape(
rootIndex,
nodes.Length,
static (rows, index) => rows[index].PositiveChildIndex,
static (rows, index) => rows[index].NegativeChildIndex,
nodes,
"physics BSP");
}
private static void ValidateChild(
int childIndex,
int nodeCount,
int ownerIndex,
string edge)
{
if (childIndex < -1 || childIndex >= nodeCount)
{
throw new InvalidDataException(
$"physics node[{ownerIndex}] {edge} child {childIndex} is out of range.");
}
}
internal static void ValidatePreOrderShape(
int rootIndex,
int nodeCount,
Func, int, int> getPositive,
Func, int, int> getNegative,
ImmutableArray nodes,
string name)
{
var visited = new bool[nodeCount];
var stack = new Stack();
stack.Push(rootIndex);
int expectedIndex = 0;
while (stack.Count != 0)
{
int index = stack.Pop();
if (visited[index])
throw new InvalidDataException($"{name} contains a cycle or shared child.");
if (index != expectedIndex)
{
throw new InvalidDataException(
$"{name} is not in deterministic positive-before-negative pre-order: " +
$"expected node {expectedIndex}, encountered {index}.");
}
visited[index] = true;
expectedIndex++;
int negative = getNegative(nodes, index);
int positive = getPositive(nodes, index);
if (negative >= 0)
stack.Push(negative);
if (positive >= 0)
stack.Push(positive);
}
if (expectedIndex != nodeCount)
{
throw new InvalidDataException(
$"{name} contains {nodeCount - expectedIndex} unreachable node(s).");
}
}
}
///
/// One node in the separate cell-containment BSP.
///
public readonly record struct FlatCellBspNode(
BSPNodeType Type,
Plane SplittingPlane,
int PositiveChildIndex,
int NegativeChildIndex,
int LeafIndex);
///
/// Immutable deterministic representation of a cell-containment BSP. It is
/// intentionally separate from physics BSP storage because it has no leaf
/// polygons, solid marker, or bounding sphere.
///
public sealed class FlatCellContainmentBsp
{
public FlatCellContainmentBsp(
int rootIndex,
ImmutableArray nodes)
{
if (nodes.IsDefault)
throw new ArgumentException("Node rows must not be default.", nameof(nodes));
if (nodes.Length == 0)
{
if (rootIndex != -1)
throw new InvalidDataException("An empty cell BSP must use root index -1.");
}
else
{
if (rootIndex != 0)
{
throw new InvalidDataException(
$"A deterministic pre-order cell BSP must start at index 0, not {rootIndex}.");
}
for (int i = 0; i < nodes.Length; i++)
{
FlatCellBspNode node = nodes[i];
FlatBspNodeContract.Validate(
node.Type,
node.PositiveChildIndex >= 0,
node.NegativeChildIndex >= 0,
$"cell node[{i}]");
if (node.PositiveChildIndex < -1 || node.PositiveChildIndex >= nodes.Length)
{
throw new InvalidDataException(
$"cell node[{i}] positive child {node.PositiveChildIndex} is out of range.");
}
if (node.NegativeChildIndex < -1 || node.NegativeChildIndex >= nodes.Length)
{
throw new InvalidDataException(
$"cell node[{i}] negative child {node.NegativeChildIndex} is out of range.");
}
}
FlatPhysicsBsp.ValidatePreOrderShape(
rootIndex,
nodes.Length,
static (rows, index) => rows[index].PositiveChildIndex,
static (rows, index) => rows[index].NegativeChildIndex,
nodes,
"cell-containment BSP");
}
RootIndex = rootIndex;
Nodes = nodes;
}
public int RootIndex { get; }
public ImmutableArray Nodes { get; }
}
///
/// Immutable Setup collision payload. List order is the DAT order.
///
public sealed class FlatSetupCollision
{
public FlatSetupCollision(
ImmutableArray cylinders,
ImmutableArray spheres,
float height,
float radius,
float stepUpHeight,
float stepDownHeight)
{
if (cylinders.IsDefault)
throw new ArgumentException("Cylinder rows must not be default.", nameof(cylinders));
if (spheres.IsDefault)
throw new ArgumentException("Sphere rows must not be default.", nameof(spheres));
Cylinders = cylinders;
Spheres = spheres;
Height = height;
Radius = radius;
StepUpHeight = stepUpHeight;
StepDownHeight = stepDownHeight;
}
public ImmutableArray Cylinders { get; }
public ImmutableArray Spheres { get; }
public float Height { get; }
public float Radius { get; }
public float StepUpHeight { get; }
public float StepDownHeight { get; }
}
public readonly record struct FlatGfxObjVisualBounds(
Vector3 Min,
Vector3 Max,
Vector3 Center,
float Radius,
Vector3 HalfExtents);
///
/// Immutable GfxObj collision payload prepared independently of rendering.
///
public sealed record FlatGfxObjCollisionAsset(
FlatPhysicsBsp PhysicsBsp,
FlatCollisionSphere? BoundingSphere,
FlatGfxObjVisualBounds? VisualBounds);
///
/// Immutable CellStruct geometry. Per-EnvCell transforms and topology are not
/// embedded so identical CellStruct geometry can be aliased safely.
///
public sealed record FlatCellStructureCollisionAsset(
FlatPhysicsBsp PhysicsBsp,
FlatCellContainmentBsp ContainmentBsp,
FlatPolygonTable PortalPolygons);
///
/// One per-EnvCell portal row. directly addresses
/// the owning .
///
public readonly record struct FlatEnvCellPortal(
ushort OtherCellId,
ushort PolygonId,
ushort Flags,
int PolygonIndex);
///
/// Per-EnvCell topology kept separate from reusable CellStruct geometry.
/// Visible cell IDs are sorted to make prepared output deterministic.
///
public sealed class FlatEnvCellTopology
{
public FlatEnvCellTopology(
ImmutableArray portals,
ImmutableArray visibleCellIds,
bool seenOutside)
{
if (portals.IsDefault)
throw new ArgumentException("Portal rows must not be default.", nameof(portals));
if (visibleCellIds.IsDefault)
{
throw new ArgumentException(
"Visible-cell rows must not be default.",
nameof(visibleCellIds));
}
uint previous = 0;
for (int i = 0; i < visibleCellIds.Length; i++)
{
uint current = visibleCellIds[i];
if (i != 0 && current <= previous)
{
throw new InvalidDataException(
"Visible cell IDs must be strictly ordered and unique.");
}
previous = current;
}
for (int i = 0; i < portals.Length; i++)
{
if (portals[i].PolygonIndex < 0)
{
throw new InvalidDataException(
$"portal[{i}] has invalid polygon index {portals[i].PolygonIndex}.");
}
}
Portals = portals;
VisibleCellIds = visibleCellIds;
SeenOutside = seenOutside;
}
public ImmutableArray Portals { get; }
public ImmutableArray VisibleCellIds { get; }
public bool SeenOutside { get; }
}
///
/// Test/shadow aggregate joining one reusable CellStruct with one EnvCell's
/// topology. Production package/publication layers keep these lifetimes
/// separate.
///
public sealed class FlatCellCollisionAsset
{
public FlatCellCollisionAsset(
FlatCellStructureCollisionAsset structure,
FlatEnvCellTopology topology)
{
ArgumentNullException.ThrowIfNull(structure);
ArgumentNullException.ThrowIfNull(topology);
int polygonCount = structure.PortalPolygons.Polygons.Length;
for (int i = 0; i < topology.Portals.Length; i++)
{
int polygonIndex = topology.Portals[i].PolygonIndex;
if ((uint)polygonIndex >= (uint)polygonCount)
{
throw new InvalidDataException(
$"portal[{i}] polygon index {polygonIndex} exceeds " +
$"portal polygon count {polygonCount}.");
}
}
Structure = structure;
Topology = topology;
}
public FlatCellStructureCollisionAsset Structure { get; }
public FlatEnvCellTopology Topology { get; }
}
internal static class FlatBspNodeContract
{
public static void Validate(
BSPNodeType type,
bool hasPositive,
bool hasNegative,
string name)
{
if (!Enum.IsDefined(type) || type == BSPNodeType.Portal)
throw new InvalidDataException($"{name} has unsupported type 0x{(uint)type:X8}.");
(bool expectedPositive, bool expectedNegative) = type switch
{
BSPNodeType.Leaf => (false, false),
BSPNodeType.BPIn or BSPNodeType.BPnn => (true, false),
BSPNodeType.BpIN or BSPNodeType.BpnN => (false, true),
BSPNodeType.BPIN or BSPNodeType.BPnN => (true, true),
BSPNodeType.BPOL or BSPNodeType.BPFL => (false, false),
_ => throw new InvalidDataException(
$"{name} has unsupported type 0x{(uint)type:X8}."),
};
if (hasPositive != expectedPositive || hasNegative != expectedNegative)
{
throw new InvalidDataException(
$"{name} type {type} requires children " +
$"(+={expectedPositive}, -={expectedNegative}) but contains " +
$"(+={hasPositive}, -={hasNegative}).");
}
}
}