using System.Numerics;
namespace AcDream.Core.Physics;
///
/// One collision-bearing shape attached to a logical PhysicsObj.
/// The set is a DISPATCH, not a union (AP-152): a BSP door emits one shape
/// per physics-BSP part and no primitive; a primitive-only object emits its
/// CylSpheres, else its Spheres; a simple item zero. Positions and rotations are
/// LOCAL to the entity's origin so
/// can re-transform them when the entity moves.
///
///
/// Retail anchor: CPhysicsPart + CPhysicsPart::find_obj_collisions
/// at acclient_2013_pseudo_c.txt:275045-275055. Each part stores its
/// own transform and dispatches per-part collision to its GfxObj.
///
///
///
/// CONSTRUCTION IS BY FACTORY ONLY (, ,
/// ) and the constructor is private. That is the AP-156
/// invariant expressed at the type rather than only at the producer: a BSP
/// shape's radius, its bounding-sphere CENTRE, and its authored bounding BOX
/// arrive as one value and are scaled together
/// inside , so no call site — present or future — can take one
/// while dropping another. Those splits are exactly what produced AP-156 (the
/// centre dropped) and #334 (the box never resolved at all); with a public
/// positional constructor a new BSP producer could reintroduce either silently
/// and green.
///
///
public readonly record struct ShadowShape
{
private ShadowShape(
uint gfxObjId,
Vector3 localPosition,
Quaternion localRotation,
float scale,
ShadowCollisionType collisionType,
float radius,
float cylHeight,
Vector3 boundsCenter,
Vector3 localBoundsMin,
Vector3 localBoundsMax)
{
GfxObjId = gfxObjId;
LocalPosition = localPosition;
LocalRotation = localRotation;
Scale = scale;
CollisionType = collisionType;
Radius = radius;
CylHeight = cylHeight;
BoundsCenter = boundsCenter;
LocalBoundsMin = localBoundsMin;
LocalBoundsMax = localBoundsMax;
}
/// Source GfxObj id, for the BSP walk and for diagnostics.
public uint GfxObjId { get; }
/// Part placement in the entity's own frame, entity-scaled.
public Vector3 LocalPosition { get; }
/// Part orientation in the entity's own frame.
public Quaternion LocalRotation { get; }
/// The entity (or part) scale already applied to the geometry.
public float Scale { get; }
public ShadowCollisionType CollisionType { get; }
/// Collision radius, entity-scaled.
public float Radius { get; }
/// Cylinder height, entity-scaled; 0 for BSP and Sphere.
public float CylHeight { get; }
///
/// Center of the shape's bounding sphere IN THE SHAPE'S OWN LOCAL FRAME —
/// the frame rotates out of and
/// is measured in — already multiplied by the entity
/// scale, like and .
/// Zero for Cylinder/Sphere shapes, whose
/// already IS their center.
///
///
/// BSP shapes need it because a GfxObj's physics BSP is authored in the
/// GfxObj's own coordinates and its root bounding sphere is frequently NOT
/// centered on that origin (376 of the 973 physics-BSP parts in the
/// installed client_portal.dat sit further from it than half their
/// own radius; worst 20.762 m on a 27.708 m sphere, gfx 0x010036DD).
/// 's flood needs the
/// sphere's real position, not the part origin's.
///
///
///
/// Retail anchor: CGfxObj::physics_sphere ([gfxobj+0x74]) is
/// assigned BSPTREE::GetSphere(physics_bsp) @0x005397e0
/// (mov eax,[ecx]; add eax,4 — the root BSPNODE's
/// CSphere sphere, past its 4-byte vftable), so retail's per-part
/// flood sphere IS the BSP root bounding sphere, origin included.
/// CEnvCell::find_transit_cells @0x0052cae0 — the part-array overload
/// reached from CPhysicsObj::find_bbox_cell_list @0x00510fc0 via
/// CPartArray::calc_cross_cells_static @0x00518160's
/// [vtbl+0x7c] dispatch — loads that sphere at
/// 0x0052cb36 mov esi,[ecx+0x74], transforms its CENTER through the
/// part's own Position at [part+0x30]
/// (0x0052cb4c add eax,0x30 / 0x0052cb5a call Position::localtolocal),
/// and only then reads the radius at 0x0052cb65 fadd [esi+0xc].
///
///
public Vector3 BoundsCenter { get; }
///
/// The shape's AXIS-ALIGNED BOX in the same local frame as
/// , already entity-scaled. For a BSP shape this
/// is retail's CGfxObj::gfx_bound_box — the AABB of the GfxObj's
/// vertex array, which CPhysicsPart::GetBoundingBox @0x0050d600
/// returns and which CLandCell::add_all_outside_cells @0x00533360
/// divides by square_length to build the outdoor cell rectangle
/// (#334). Primitive shapes carry their own radius/height box; they never
/// reach that path, because CPhysicsObj::calc_cross_cells
/// @0x00515230 routes only HAS_PHYSICS_BSP_PS objects to
/// find_bbox_cell_list.
///
public Vector3 LocalBoundsMin { get; }
///
public Vector3 LocalBoundsMax { get; }
///
/// One physics-BSP part. carries the part
/// GfxObj's physics-BSP ROOT bounding sphere AND its vertex-array box in
/// the GfxObj's OWN frame, unscaled — retail's
/// CGfxObj::physics_sphere and CGfxObj::gfx_bound_box.
/// Sphere radius, sphere centre, and both box corners are scaled together
/// here, which is the whole point of taking them as one value: retail
/// reads the sphere for the indoor portal reject and the box for the
/// outdoor extent walk, and a producer that supplied one without the
/// other would silently force a substitute at the other call site
/// (AP-156, then #334).
///
public static ShadowShape Bsp(
uint gfxObjId,
Vector3 localPosition,
Quaternion localRotation,
float scale,
ShadowPartGeometry localGeometry)
=> new(
gfxObjId,
localPosition,
localRotation,
scale,
ShadowCollisionType.BSP,
localGeometry.Sphere.Radius * scale,
0f,
localGeometry.Sphere.Origin * scale,
localGeometry.BoxMin * scale,
localGeometry.BoxMax * scale);
///
/// One Setup CylSphere. already IS the
/// shape's centre, so it carries no separate bounds centre.
///
public static ShadowShape Cylinder(
uint gfxObjId,
Vector3 localPosition,
Quaternion localRotation,
float scale,
float radius,
float cylHeight)
=> new(
gfxObjId,
localPosition,
localRotation,
scale,
ShadowCollisionType.Cylinder,
radius,
cylHeight,
Vector3.Zero,
new Vector3(-radius, -radius, 0f),
new Vector3(radius, radius, cylHeight));
///
/// One Setup Sphere. already IS the
/// shape's centre, so it carries no separate bounds centre.
///
public static ShadowShape Sphere(
uint gfxObjId,
Vector3 localPosition,
Quaternion localRotation,
float scale,
float radius)
=> new(
gfxObjId,
localPosition,
localRotation,
scale,
ShadowCollisionType.Sphere,
radius,
0f,
Vector3.Zero,
new Vector3(-radius),
new Vector3(radius));
}