using System.Numerics; using AcDream.Core.Physics; namespace AcDream.App.Rendering; /// /// backed by the player's swept-sphere /// engine. Ports retail's SmartBox::update_viewer (0x00453ce0): sweep /// the 0.3 m viewer_sphere from the head-pivot to the desired eye via a /// CTransition and use the stopped position. Reusing /// collides against indoor /// cell walls (FindEnvCollisions) AND outdoor/baked GfxObj shells /// (FindObjCollisions) in one faithful path. /// public sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe { /// Retail viewer_sphere radius (acclient :93314). public const float ViewerSphereRadius = 0.3f; private readonly PhysicsEngine _physics; public PhysicsCameraCollisionProbe(PhysicsEngine physics) => _physics = physics; public Vector3 SweepEye(Vector3 pivot, Vector3 desiredEye, uint cellId, uint selfEntityId) { // No starting cell → nothing to sweep against; keep the desired eye. if (cellId == 0) return desiredEye; // SpherePath.InitPath puts sphere0's center at pathPos + (0,0,radius) // (the player foot-capsule convention). Retail's viewer_sphere center is // (0,0,0), so shift the path DOWN by the radius to make the SPHERE CENTER // travel pivot→eye, then add it back to the swept stop position. Vector3 begin = ToSpherePath(pivot, ViewerSphereRadius); Vector3 end = ToSpherePath(desiredEye, ViewerSphereRadius); var r = _physics.ResolveWithTransition( currentPos: begin, targetPos: end, cellId: cellId, sphereRadius: ViewerSphereRadius, sphereHeight: 0f, // single sphere (no head sphere) stepUpHeight: 0f, // no step-up for a camera stepDownHeight: 0f, // no step-down / ground snap isOnGround: false, // no contact-plane / walkable semantics body: null, // no cross-frame persistence // Retail SmartBox::update_viewer calls init_object(player, 0x5c) = // IsViewer | PathClipped | FreeRotate | PerfectClip (acclient // pseudo-C :92864; enum TransitionTypes.cs:24-33). PathClipped makes // the sweep HARD-STOP at first contact (TransitionTypes.cs:811) — the // spring-arm pull-in, not the player's edge-slide. IsViewer lets the // eye pass through creatures, colliding only with world geometry // (CollisionExemption.cs:83-85). FreeRotate/PerfectClip are no-ops in // acdream today but set to match retail's exact value. NOT IsPlayer // (0x100), so camera sweeps stay out of the #98 capture filter. moverFlags: ObjectInfoState.IsViewer | ObjectInfoState.PathClipped | ObjectInfoState.FreeRotate | ObjectInfoState.PerfectClip, movingEntityId: selfEntityId); // skip the player's own ShadowEntry Vector3 eye = FromSpherePath(r.Position, ViewerSphereRadius); // Phase U.4c spike apparatus (THROWAWAY — strip with ACDREAM_PROBE_FLAP). // The post-fix [flap-cam] capture shows the eye flying to full chase distance // (eyeInRoot=n ~90%) in cells like 0xA9B40174/0175 — i.e. this sweep is not // stopping it. This line answers WHY, the fork that picks the primary residual // fix: pulledIn≈0 with resolved=Y bsp=ok ⇒ the sweep ran but found NOTHING in // that cell (space genuinely open, or wall geometry the per-cell sweep can't // reach → clip-robustness is primary); resolved=n / bsp=nobsp/noroot ⇒ collision // can't even run there (cell/BSP not loaded → camera-collision reliability is // primary); pulledIn large ⇒ collision IS engaging (eye leaving is then expected // through an opening). Paired per-frame with the builder's [flap]/[flap-cam]. if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled) { var cp = _physics.DataCache?.GetCellStruct(cellId); string bsp = cp?.BSP is null ? "nobsp" : (cp.BSP.Root is null ? "noroot" : "ok"); float desiredBack = Vector3.Distance(pivot, desiredEye); float eyeBack = Vector3.Distance(pivot, eye); System.Console.WriteLine( $"[flap-sweep] cell=0x{cellId:X8} resolved={(cp is not null ? "Y" : "n")} bsp={bsp} " + $"desiredBack={desiredBack:F2} eyeBack={eyeBack:F2} pulledIn={desiredBack - eyeBack:F2} " + $"collNormValid={r.CollisionNormalValid}"); } return eye; } /// Eye/pivot point → InitPath path point (subtract the sphere-center offset). internal static Vector3 ToSpherePath(Vector3 spherePoint, float radius) => spherePoint - new Vector3(0f, 0f, radius); /// InitPath path point → eye point (add the sphere-center offset back). internal static Vector3 FromSpherePath(Vector3 pathPoint, float radius) => pathPoint + new Vector3(0f, 0f, radius); }