using System;
using System.Collections.Generic;
using System.Numerics;
using AcDream.Core.World;
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.
/// #175: per-part pose override for the
/// BSP part shapes — the entity's motion-table DEFAULT-STATE pose (the
/// closed pose for doors). Retail collision tests each part's LIVE
/// CPhysicsPart pose, which for an idle entity is the motion
/// table's default state, NOT the Setup's placement frame — the two
/// differ on e.g. the Facility Hub double door (Setup 0x02000C9D:
/// placement poses the panels AJAR at yaw −150°/−30°, y −0.44 m; the
/// closed pose is straight). Null / short lists fall back to the
/// placement frame per part (entities with no motion table, and the
/// CylSphere/Sphere shapes, are unaffected — retail poses those from
/// the setup too).
public static IReadOnlyList FromSetup(
Setup setup,
float entScale,
Func hasPhysicsBsp,
IReadOnlyList? partPoseOverride = null)
{
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 true Sphere (no height clamping).
// Retail anchor: CSphere::intersects_sphere @ 0x00537A80 uses 3-D distance
// for the overlap check, unlike CCylSphere which clips to [low_pt, high_pt].
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.Sphere,
Radius: sph.Radius * entScale,
CylHeight: 0f));
}
}
// 3. Parts — one BSP shape per part with a non-null PhysicsBSP.
// Pose priority per part: partPoseOverride (the motion-table
// default-state pose, #175) → placement frame → identity.
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;
if (partPoseOverride is not null && i < partPoseOverride.Count)
partFrame = partPoseOverride[i];
else if (placementFrame is not null && i < placementFrame.Frames.Count)
partFrame = placementFrame.Frames[i];
else
partFrame = 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;
}
///
/// #185: build BSP shapes for a landblock-baked multi-part entity (buildings,
/// stair runs, fences, rock clusters) from its per-part s,
/// for registration via
/// under the entity's SINGLE unique id.
///
///
/// Replaces the former per-part Register(entity.Id * 256u + partIndex)
/// (GameWindow.cs) whose * 256u OVERFLOWED uint32 for class-prefixed
/// landblock ids (0x40/0x80/0xC0…): the overflow dropped
/// the prefix byte, so different-class entities sharing the low 24 bits
/// collided on one shadow part-id and Register's deregister-then-insert
/// silently overwrote one entity's collision geometry — the #185 "invisible
/// wall half-way up the stairs" (rendered steps with no collision).
///
///
///
/// Retail anchor: a multi-part object is one CPhysicsObj + CPartArray;
/// CPhysicsObj::add_shadows_to_cells (0x00514ae0) → CPartArray::AddPartsShadow
/// walks the part array under the single object — no synthetic per-part id.
///
///
///
/// Each part's local transform comes from its
/// (root-relative), decomposed to LocalPosition/LocalRotation/Scale;
/// RegisterMultiPart reconstructs the world placement identically
/// (entityWorldPos + rotate(LocalPosition, entityWorldRot)). Building
/// shells are excluded — they collide via the per-LandCell building channel
/// (CSortCell::find_collisions), not as shadow objects.
///
///
/// The entity's per-part mesh references.
/// True for LandBlockInfo.Buildings[] shells.
/// Resolves a GfxObj id to its cached physics (BSP +
/// bounding sphere). Production: id => cache.GetGfxObj(id).
public static List FromLandblockBspParts(
IReadOnlyList meshRefs,
bool isBuildingShell,
Func getGfxObj)
{
if (getGfxObj is null) throw new ArgumentNullException(nameof(getGfxObj));
var shapes = new List();
// Building shells collide via the building channel (retail), not shadow objects.
if (isBuildingShell || meshRefs is null) return shapes;
foreach (var meshRef in meshRefs)
{
var phys = getGfxObj(meshRef.GfxObjId);
if (phys?.BSP?.Root is null) continue; // no physics BSP → nothing to collide
// PartTransform is root-relative; decompose to local pos/rot/scale.
if (!Matrix4x4.Decompose(meshRef.PartTransform,
out var pScale, out var pRot, out var pPos))
{
pScale = Vector3.One;
pRot = Quaternion.Identity;
pPos = new Vector3(meshRef.PartTransform.M41,
meshRef.PartTransform.M42,
meshRef.PartTransform.M43);
}
float partScale = pScale.X > 0f ? pScale.X : 1f; // AC objects are uniformly scaled
float localRadius = phys.BoundingSphere?.Radius ?? 1f;
shapes.Add(new ShadowShape(
GfxObjId: meshRef.GfxObjId,
LocalPosition: pPos,
LocalRotation: pRot,
Scale: partScale,
CollisionType: ShadowCollisionType.BSP,
Radius: localRadius * partScale,
CylHeight: 0f));
}
return shapes;
}
/// 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;
}
}