feat(content): bake and read flat collision assets
Append strict collision/topology payloads to the existing prepared package so later physics cutover can drop parsed DAT graphs without adding a second mapping or changing traversal behavior. The full 2,232,170-key catalog is deterministic across worker counts, exact-byte aliased, corruption-isolated, and cancellation-safe.
This commit is contained in:
parent
d9300c7854
commit
9cd42417a8
29 changed files with 2868 additions and 63 deletions
|
|
@ -1,6 +1,8 @@
|
|||
using System.Collections.Immutable;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Enums;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.Core.Physics;
|
||||
|
|
@ -15,6 +17,13 @@ namespace AcDream.Core.Physics;
|
|||
/// </summary>
|
||||
public static class FlatCollisionAssetBuilder
|
||||
{
|
||||
private static FlatPhysicsBsp EmptyPhysicsBsp() =>
|
||||
new(
|
||||
-1,
|
||||
ImmutableArray<FlatPhysicsBspNode>.Empty,
|
||||
ImmutableArray<int>.Empty,
|
||||
FlatPolygonTable.Empty);
|
||||
|
||||
public static FlatPhysicsBsp FlattenPhysicsBsp(
|
||||
PhysicsBSPNode? root,
|
||||
IReadOnlyDictionary<ushort, ResolvedPolygon> resolved)
|
||||
|
|
@ -270,6 +279,53 @@ public static class FlatCollisionAssetBuilder
|
|||
source.StepDownHeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares the exact Setup collision fields used by
|
||||
/// <see cref="PhysicsDataCache.CacheSetup"/> without retaining the parsed
|
||||
/// DAT object. Source list order and every float bit are preserved.
|
||||
/// </summary>
|
||||
public static FlatSetupCollision FlattenSetup(Setup source)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
|
||||
var cylinders = ImmutableArray.CreateBuilder<FlatCollisionCylinder>(
|
||||
source.CylSpheres?.Count ?? 0);
|
||||
if (source.CylSpheres is not null)
|
||||
{
|
||||
foreach (CylSphere cylinder in source.CylSpheres)
|
||||
{
|
||||
if (cylinder is null)
|
||||
throw new InvalidDataException("Setup cylinder row is null.");
|
||||
cylinders.Add(new FlatCollisionCylinder(
|
||||
cylinder.Origin,
|
||||
cylinder.Radius,
|
||||
cylinder.Height));
|
||||
}
|
||||
}
|
||||
|
||||
var spheres = ImmutableArray.CreateBuilder<FlatCollisionSphere>(
|
||||
source.Spheres?.Count ?? 0);
|
||||
if (source.Spheres is not null)
|
||||
{
|
||||
foreach (Sphere sphere in source.Spheres)
|
||||
{
|
||||
if (sphere is null)
|
||||
throw new InvalidDataException("Setup sphere row is null.");
|
||||
spheres.Add(new FlatCollisionSphere(
|
||||
sphere.Origin,
|
||||
sphere.Radius));
|
||||
}
|
||||
}
|
||||
|
||||
return new FlatSetupCollision(
|
||||
cylinders.MoveToImmutable(),
|
||||
spheres.MoveToImmutable(),
|
||||
source.Height,
|
||||
source.Radius,
|
||||
source.StepUpHeight,
|
||||
source.StepDownHeight);
|
||||
}
|
||||
|
||||
public static FlatGfxObjCollisionAsset FlattenGfxObj(
|
||||
GfxObjPhysics source,
|
||||
GfxObjVisualBounds? visualBounds = null)
|
||||
|
|
@ -297,6 +353,139 @@ public static class FlatCollisionAssetBuilder
|
|||
flatVisualBounds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares one parsed GfxObj and drops the source graph. Objects without
|
||||
/// a retail physics BSP still retain their visual bounds because the
|
||||
/// current runtime uses those bounds as its collision fallback.
|
||||
/// </summary>
|
||||
public static FlatGfxObjCollisionAsset FlattenGfxObj(GfxObj source)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
|
||||
GfxObjVisualBounds? visualBounds = source.VertexArray is null
|
||||
? null
|
||||
: PhysicsDataCache.ComputeVisualBounds(source.VertexArray);
|
||||
FlatGfxObjVisualBounds? flatVisualBounds = visualBounds is null
|
||||
? null
|
||||
: new FlatGfxObjVisualBounds(
|
||||
visualBounds.Min,
|
||||
visualBounds.Max,
|
||||
visualBounds.Center,
|
||||
visualBounds.Radius,
|
||||
visualBounds.HalfExtents);
|
||||
|
||||
bool hasPhysics =
|
||||
source.Flags.HasFlag(GfxObjFlags.HasPhysics)
|
||||
&& source.PhysicsBSP?.Root is not null
|
||||
&& source.VertexArray is not null;
|
||||
if (!hasPhysics)
|
||||
{
|
||||
return new FlatGfxObjCollisionAsset(
|
||||
EmptyPhysicsBsp(),
|
||||
null,
|
||||
flatVisualBounds);
|
||||
}
|
||||
|
||||
Dictionary<ushort, ResolvedPolygon> resolved =
|
||||
PhysicsDataCache.ResolvePolygons(
|
||||
source.PhysicsPolygons,
|
||||
source.VertexArray!);
|
||||
Sphere boundingSphere = source.PhysicsBSP!.Root!.BoundingSphere
|
||||
?? throw new InvalidDataException(
|
||||
"GfxObj physics BSP root has no bounding sphere.");
|
||||
|
||||
return new FlatGfxObjCollisionAsset(
|
||||
FlattenPhysicsBsp(source.PhysicsBSP.Root, resolved),
|
||||
new FlatCollisionSphere(
|
||||
boundingSphere.Origin,
|
||||
boundingSphere.Radius),
|
||||
flatVisualBounds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares reusable CellStruct collision geometry independently of any
|
||||
/// EnvCell placement or topology.
|
||||
/// </summary>
|
||||
public static FlatCellStructureCollisionAsset FlattenCellStructure(
|
||||
CellStruct source)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
|
||||
FlatPhysicsBsp physicsBsp;
|
||||
if (source.PhysicsBSP?.Root is null)
|
||||
{
|
||||
physicsBsp = EmptyPhysicsBsp();
|
||||
}
|
||||
else
|
||||
{
|
||||
Dictionary<ushort, ResolvedPolygon> resolved =
|
||||
PhysicsDataCache.ResolvePolygons(
|
||||
source.PhysicsPolygons,
|
||||
source.VertexArray);
|
||||
physicsBsp = FlattenPhysicsBsp(
|
||||
source.PhysicsBSP.Root,
|
||||
resolved);
|
||||
}
|
||||
|
||||
Dictionary<ushort, ResolvedPolygon> portalResolved =
|
||||
PhysicsDataCache.ResolvePolygons(
|
||||
source.Polygons,
|
||||
source.VertexArray);
|
||||
return new FlatCellStructureCollisionAsset(
|
||||
physicsBsp,
|
||||
FlattenCellContainmentBsp(source.CellBSP?.Root),
|
||||
FlattenPolygonTable(portalResolved));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares the per-cell portion of collision publication. Other-cell IDs
|
||||
/// are expanded with the owning landblock exactly as
|
||||
/// <see cref="PhysicsDataCache.CacheCellStruct"/> does.
|
||||
/// </summary>
|
||||
public static FlatEnvCellTopology FlattenEnvCellTopology(
|
||||
uint envCellId,
|
||||
EnvCell source,
|
||||
FlatPolygonTable portalPolygons)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
ArgumentNullException.ThrowIfNull(portalPolygons);
|
||||
|
||||
var portals = ImmutableArray.CreateBuilder<FlatEnvCellPortal>(
|
||||
source.CellPortals.Count);
|
||||
foreach (CellPortal portal in source.CellPortals)
|
||||
{
|
||||
ushort polygonId = portal.PolygonId;
|
||||
if (!portalPolygons.TryFindPolygonIndex(
|
||||
polygonId,
|
||||
out int polygonIndex))
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"EnvCell 0x{envCellId:X8} portal references missing " +
|
||||
$"polygon 0x{polygonId:X4}.");
|
||||
}
|
||||
|
||||
portals.Add(new FlatEnvCellPortal(
|
||||
portal.OtherCellId,
|
||||
polygonId,
|
||||
(ushort)portal.Flags,
|
||||
polygonIndex));
|
||||
}
|
||||
|
||||
uint prefix = envCellId & 0xFFFF_0000u;
|
||||
uint[] visibleIds = source.VisibleCells is null
|
||||
? []
|
||||
: source.VisibleCells
|
||||
.Select(id => prefix | id)
|
||||
.Distinct()
|
||||
.Order()
|
||||
.ToArray();
|
||||
|
||||
return new FlatEnvCellTopology(
|
||||
portals.MoveToImmutable(),
|
||||
ImmutableArray.Create(visibleIds),
|
||||
source.Flags.HasFlag(EnvCellFlags.SeenOutside));
|
||||
}
|
||||
|
||||
public static FlatCellCollisionAsset FlattenCell(CellPhysics source)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
|
|
@ -310,9 +499,31 @@ public static class FlatCollisionAssetBuilder
|
|||
source.PortalPolygons
|
||||
?? new Dictionary<ushort, ResolvedPolygon>());
|
||||
|
||||
var portals = ImmutableArray.CreateBuilder<FlatEnvCellPortal>(
|
||||
source.Portals.Count);
|
||||
foreach (PortalInfo portal in source.Portals)
|
||||
FlatEnvCellTopology topology = FlattenEnvCellTopology(
|
||||
source.Portals,
|
||||
source.VisibleCellIds,
|
||||
source.SeenOutside,
|
||||
portalPolygons);
|
||||
var structure = new FlatCellStructureCollisionAsset(
|
||||
physicsBsp,
|
||||
containmentBsp,
|
||||
portalPolygons);
|
||||
return new FlatCellCollisionAsset(structure, topology);
|
||||
}
|
||||
|
||||
public static FlatEnvCellTopology FlattenEnvCellTopology(
|
||||
IReadOnlyList<PortalInfo> portals,
|
||||
IEnumerable<uint> visibleCellIds,
|
||||
bool seenOutside,
|
||||
FlatPolygonTable portalPolygons)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(portals);
|
||||
ArgumentNullException.ThrowIfNull(visibleCellIds);
|
||||
ArgumentNullException.ThrowIfNull(portalPolygons);
|
||||
|
||||
var flatPortals = ImmutableArray.CreateBuilder<FlatEnvCellPortal>(
|
||||
portals.Count);
|
||||
foreach (PortalInfo portal in portals)
|
||||
{
|
||||
if (!portalPolygons.TryFindPolygonIndex(
|
||||
portal.PolygonId,
|
||||
|
|
@ -323,24 +534,21 @@ public static class FlatCollisionAssetBuilder
|
|||
$"0x{portal.PolygonId:X4}.");
|
||||
}
|
||||
|
||||
portals.Add(new FlatEnvCellPortal(
|
||||
flatPortals.Add(new FlatEnvCellPortal(
|
||||
portal.OtherCellId,
|
||||
portal.PolygonId,
|
||||
portal.Flags,
|
||||
polygonIndex));
|
||||
}
|
||||
|
||||
uint[] visibleIds = source.VisibleCellIds.ToArray();
|
||||
Array.Sort(visibleIds);
|
||||
var topology = new FlatEnvCellTopology(
|
||||
portals.MoveToImmutable(),
|
||||
uint[] visibleIds = visibleCellIds
|
||||
.Distinct()
|
||||
.Order()
|
||||
.ToArray();
|
||||
return new FlatEnvCellTopology(
|
||||
flatPortals.MoveToImmutable(),
|
||||
ImmutableArray.Create(visibleIds),
|
||||
source.SeenOutside);
|
||||
var structure = new FlatCellStructureCollisionAsset(
|
||||
physicsBsp,
|
||||
containmentBsp,
|
||||
portalPolygons);
|
||||
return new FlatCellCollisionAsset(structure, topology);
|
||||
seenOutside);
|
||||
}
|
||||
|
||||
private static void AttachChild(
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ public sealed class PhysicsDataCache
|
|||
/// Used as a fallback collision shape for entities whose Setup has no
|
||||
/// physics data — we approximate collision using the visual extent.
|
||||
/// </summary>
|
||||
private static GfxObjVisualBounds ComputeVisualBounds(VertexArray vertexArray)
|
||||
internal static GfxObjVisualBounds ComputeVisualBounds(VertexArray vertexArray)
|
||||
{
|
||||
if (vertexArray.Vertices == null || vertexArray.Vertices.Count == 0)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue