The new value type collided with DatReaderWriter.Types.Frame (used in physics-adjacent code like ShadowShapeBuilder), which the structural fix (per-file using-aliases across 6 files) would have re-incurred in every later physics slice. Renamed the TYPE to CellFrame; the Position.Frame MEMBER keeps retail's name. Restored the 5 alias-only files to their pre-Slice-1 state; synced spec + plan. Core 1522 passed / 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
121 lines
5.2 KiB
C#
121 lines
5.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using DatReaderWriter.DBObjs;
|
|
using DatReaderWriter.Enums;
|
|
using DatReaderWriter.Types;
|
|
|
|
namespace AcDream.Core.Physics;
|
|
|
|
/// <summary>
|
|
/// Pure-function builder that translates a <see cref="Setup"/> into a list of
|
|
/// <see cref="ShadowShape"/>s suitable for registration via
|
|
/// <see cref="ShadowObjectRegistry.RegisterMultiPart"/>.
|
|
///
|
|
/// <para>
|
|
/// Walks (1) every CylSphere → Cylinder shape, (2) every Sphere ONLY when no
|
|
/// CylSpheres are present (matches retail and the existing landblock-static
|
|
/// convention at GameWindow.cs:6034), and (3) every Part whose GfxObj has a
|
|
/// non-null PhysicsBSP → per-part BSP shape, with local transforms from
|
|
/// PlacementFrames[Resting | Default | first available].
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Retail anchor: <c>CPhysicsObj::FindObjCollisions</c> calls
|
|
/// <c>CPartArray::FindObjCollisions</c> which iterates parts; each part's
|
|
/// <c>find_obj_collisions</c> tests CylSpheres + GfxObj BSP. We emit one
|
|
/// ShadowShape per part contribution so the existing FindObjCollisions
|
|
/// iteration loop in <see cref="Transition"/> tests each part independently.
|
|
/// </para>
|
|
/// </summary>
|
|
public static class ShadowShapeBuilder
|
|
{
|
|
/// <summary>
|
|
/// Build the shape list for a Setup.
|
|
/// </summary>
|
|
/// <param name="setup">The Setup to walk.</param>
|
|
/// <param name="entScale">The entity's overall scale factor; multiplies
|
|
/// every radius, height, and local offset.</param>
|
|
/// <param name="hasPhysicsBsp">Predicate: does the GfxObj with this id
|
|
/// have a non-null PhysicsBSP? Production: <c>id => cache.GetGfxObj(id)?.BSP?.Root is not null</c>.</param>
|
|
public static IReadOnlyList<ShadowShape> FromSetup(
|
|
Setup setup,
|
|
float entScale,
|
|
Func<uint, bool> hasPhysicsBsp)
|
|
{
|
|
if (setup is null) throw new ArgumentNullException(nameof(setup));
|
|
if (hasPhysicsBsp is null) throw new ArgumentNullException(nameof(hasPhysicsBsp));
|
|
|
|
var result = new List<ShadowShape>();
|
|
|
|
// 1. CylSpheres — each becomes a Cylinder shape.
|
|
foreach (var cyl in setup.CylSpheres)
|
|
{
|
|
if (cyl.Radius <= 0f) continue;
|
|
float baseHeight = cyl.Height > 0f ? cyl.Height : cyl.Radius * 4f;
|
|
result.Add(new ShadowShape(
|
|
GfxObjId: 0u,
|
|
LocalPosition: new Vector3(cyl.Origin.X, cyl.Origin.Y, cyl.Origin.Z) * entScale,
|
|
LocalRotation: Quaternion.Identity,
|
|
Scale: entScale,
|
|
CollisionType: ShadowCollisionType.Cylinder,
|
|
Radius: cyl.Radius * entScale,
|
|
CylHeight: baseHeight * entScale));
|
|
}
|
|
|
|
// 2. Spheres — only when no CylSpheres (matches landblock-static convention
|
|
// at GameWindow.cs:6034). Each becomes a short Cylinder.
|
|
if (setup.CylSpheres.Count == 0)
|
|
{
|
|
foreach (var sph in setup.Spheres)
|
|
{
|
|
if (sph.Radius <= 0f) continue;
|
|
result.Add(new ShadowShape(
|
|
GfxObjId: 0u,
|
|
LocalPosition: new Vector3(sph.Origin.X, sph.Origin.Y, sph.Origin.Z) * entScale,
|
|
LocalRotation: Quaternion.Identity,
|
|
Scale: entScale,
|
|
CollisionType: ShadowCollisionType.Cylinder,
|
|
Radius: sph.Radius * entScale,
|
|
CylHeight: sph.Radius * 2f * entScale));
|
|
}
|
|
}
|
|
|
|
// 3. Parts — one BSP shape per part with a non-null PhysicsBSP.
|
|
AnimationFrame? placementFrame = ResolvePlacementFrame(setup);
|
|
for (int i = 0; i < setup.Parts.Count; i++)
|
|
{
|
|
uint gfxId = (uint)setup.Parts[i];
|
|
if (!hasPhysicsBsp(gfxId)) continue;
|
|
|
|
Frame partFrame = placementFrame is not null && i < placementFrame.Frames.Count
|
|
? placementFrame.Frames[i]
|
|
: new Frame { Origin = Vector3.Zero, Orientation = Quaternion.Identity };
|
|
|
|
// BSP radius default; caller substitutes the real BoundingSphere.Radius
|
|
// at registration time when available. Loose-but-safe broadphase value.
|
|
float bspRadius = 2f * entScale;
|
|
|
|
result.Add(new ShadowShape(
|
|
GfxObjId: gfxId,
|
|
LocalPosition: new Vector3(partFrame.Origin.X, partFrame.Origin.Y, partFrame.Origin.Z) * entScale,
|
|
LocalRotation: partFrame.Orientation,
|
|
Scale: entScale,
|
|
CollisionType: ShadowCollisionType.BSP,
|
|
Radius: bspRadius,
|
|
CylHeight: 0f));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>Resolve the placement frame in priority Resting → Default →
|
|
/// first available. Mirrors <c>SetupMesh.Flatten</c>'s convention.</summary>
|
|
private static AnimationFrame? ResolvePlacementFrame(Setup setup)
|
|
{
|
|
if (setup.PlacementFrames.TryGetValue(Placement.Resting, out var resting)) return resting;
|
|
if (setup.PlacementFrames.TryGetValue(Placement.Default, out var def)) return def;
|
|
foreach (var kvp in setup.PlacementFrames) return kvp.Value;
|
|
return null;
|
|
}
|
|
}
|