using System; using System.Collections.Generic; using System.Numerics; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; using DatReaderWriter.Types; namespace AcDream.Core.Physics; /// /// Pure-function builder that translates a into a list of /// s suitable for registration via /// . /// /// /// 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]. /// /// /// /// Retail anchor: CPhysicsObj::FindObjCollisions calls /// CPartArray::FindObjCollisions which iterates parts; each part's /// find_obj_collisions tests CylSpheres + GfxObj BSP. We emit one /// ShadowShape per part contribution so the existing FindObjCollisions /// iteration loop in tests each part independently. /// /// public static class ShadowShapeBuilder { /// /// Build the shape list for a Setup. /// /// The Setup to walk. /// The entity's overall scale factor; multiplies /// every radius, height, and local offset. /// Predicate: does the GfxObj with this id /// have a non-null PhysicsBSP? Production: id => cache.GetGfxObj(id)?.BSP?.Root is not null. public static IReadOnlyList FromSetup( Setup setup, float entScale, Func hasPhysicsBsp) { if (setup is null) throw new ArgumentNullException(nameof(setup)); if (hasPhysicsBsp is null) throw new ArgumentNullException(nameof(hasPhysicsBsp)); var result = new List(); // 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; } /// Resolve the placement frame in priority Resting → Default → /// first available. Mirrors SetupMesh.Flatten's convention. 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; } }