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