using System; using System.Numerics; using AcDream.Core.Physics; using AcDream.Core.Physics.Motion; namespace AcDream.App.Rendering; /// /// R5-V2 — the App-side per entity: acdream's /// stand-in for retail's CPhysicsObj as the movement managers see it. /// One is built per entity (a remote RemoteMotion or the local player) /// in GameWindow.EnsureRemoteMotionBindings / EnterPlayerModeNow /// and registered in GameWindow._physicsHosts (guid → host), so /// can resolve OTHER entities' hosts — the /// cross-entity delivery path the voyeur system /// needs. /// /// Owns a (retail /// CPhysicsObj::target_manager). Its set_target/clear_target/ /// add_voyeur/remove_voyeur/receive_target_update seams /// forward to it exactly as retail's CPhysicsObj does; the movement managers' /// target seams are repointed here, replacing the AP-79 poll adapter. The /// per-entity accessors (position/velocity/radius/contact/clocks) and the /// fan-out are injected by GameWindow so this /// class stays free of GameWindow's internals (code-structure rule #1). /// /// R5-V3: owns a too (retail /// CPhysicsObj::position_manager — retail creates it lazily via /// get_position_manager; acdream constructs it eagerly, which is /// behaviorally identical because the empty facade no-ops until its first /// StickTo/ConstrainTo). fans /// deliveries to the injected MoveToManager fan FIRST, then the /// PositionManager — retail CPhysicsObj::HandleUpdateTarget order /// (0x00512bc0: MovementManager @0x00512bf0, PositionManager /// @0x00512c1a). /// public sealed class EntityPhysicsHost : IPhysicsObjHost { private readonly Func _getPosition; private readonly Func _getVelocity; private readonly Func _getRadius; private readonly Func _inContact; private readonly Func _minterpMaxSpeed; private readonly Func _curTime; private readonly Func _physicsTimerTime; private readonly Func _getObjectA; private readonly Action _handleUpdateTarget; private readonly Action _interruptCurrentMovement; private readonly TargetManager _targetManager; public EntityPhysicsHost( uint id, Func getPosition, Func getVelocity, Func getRadius, Func inContact, Func minterpMaxSpeed, Func curTime, Func physicsTimerTime, Func getObjectA, Action handleUpdateTarget, Action interruptCurrentMovement) { Id = id; _getPosition = getPosition ?? throw new ArgumentNullException(nameof(getPosition)); _getVelocity = getVelocity ?? throw new ArgumentNullException(nameof(getVelocity)); _getRadius = getRadius ?? throw new ArgumentNullException(nameof(getRadius)); _inContact = inContact ?? throw new ArgumentNullException(nameof(inContact)); _minterpMaxSpeed = minterpMaxSpeed ?? throw new ArgumentNullException(nameof(minterpMaxSpeed)); _curTime = curTime ?? throw new ArgumentNullException(nameof(curTime)); _physicsTimerTime = physicsTimerTime ?? throw new ArgumentNullException(nameof(physicsTimerTime)); _getObjectA = getObjectA ?? throw new ArgumentNullException(nameof(getObjectA)); _handleUpdateTarget = handleUpdateTarget ?? throw new ArgumentNullException(nameof(handleUpdateTarget)); _interruptCurrentMovement = interruptCurrentMovement ?? throw new ArgumentNullException(nameof(interruptCurrentMovement)); _targetManager = new TargetManager(this); PositionManager = new PositionManager(this); } // ── IPhysicsObjHost accessors ────────────────────────────────────────── public uint Id { get; } public Position Position => _getPosition(); public Vector3 Velocity => _getVelocity(); public float Radius => _getRadius(); public bool InContact => _inContact(); public float? MinterpMaxSpeed => _minterpMaxSpeed(); public double CurTime => _curTime(); public double PhysicsTimerTime => _physicsTimerTime(); /// The owned voyeur manager (retail /// CPhysicsObj::target_manager). public TargetManager TargetManager => _targetManager; /// R5-V3 — the owned facade (retail /// CPhysicsObj::position_manager): sticky follow + (unarmed) /// constraint leash. Seam targets: MoveToManager.StickTo/Unstick, /// MotionInterpreter.UnstickFromObject, the per-tick /// AdjustOffset/UseTime drivers. public PositionManager PositionManager { get; } // ── IPhysicsObjHost fan-out / target-tracking seams ──────────────────── public IPhysicsObjHost? GetObjectA(uint id) => _getObjectA(id); public void HandleUpdateTarget(TargetInfo info) { // Retail CPhysicsObj::HandleUpdateTarget (0x00512bc0) fan order: // MovementManager (the injected MoveToManager fan) first, then // PositionManager (@0x00512c1a — the R5-V3 sticky consumer). _handleUpdateTarget(info); PositionManager.HandleUpdateTarget(info); } public void InterruptCurrentMovement() => _interruptCurrentMovement(); public void SetTarget(uint contextId, uint objectId, float radius, double quantum) => _targetManager.SetTarget(contextId, objectId, radius, quantum); public void ClearTarget() => _targetManager.ClearTarget(); public void ReceiveTargetUpdate(TargetInfo info) => _targetManager.ReceiveUpdate(info); public void AddVoyeur(uint watcherId, float radius, double quantum) => _targetManager.AddVoyeur(watcherId, radius, quantum); public void RemoveVoyeur(uint watcherId) => _targetManager.RemoveVoyeur(watcherId); // ── per-tick driver + lifecycle (called by GameWindow) ───────────────── /// Retail TargetManager::HandleTargetting — the per-tick /// voyeur sweep (self-throttled to 0.5 s). Retail runs it unconditionally /// for every entity in UpdateObjectInternal, BEFORE the movement /// managers' UseTime. public void HandleTargetting() => _targetManager.HandleTargetting(); /// Retail CPhysicsObj::exit_world's /// TargetManager::NotifyVoyeurOfEvent(ExitWorld) — tell every /// watcher of this entity that it left the world (they drop the /// stick/moveto). Called on despawn before the host is removed from the /// registry. public void NotifyExitWorld() => _targetManager.NotifyVoyeurOfEvent(TargetStatus.ExitWorld); /// R5-V3 (#171): retail CPhysicsObj::teleport_hook's tail /// (0x00514ed0 @0x00514f1b-0x00514f28) — TargetManager::ClearTarget /// (drop this entity's OWN subscription) then /// NotifyVoyeurOfEvent(Teleported) (every entity watching THIS one /// drops its stick/moveto — StickyManager::HandleUpdateTarget's /// non-Ok teardown path). Called after a teleport placement. public void NotifyTeleported() { _targetManager.ClearTarget(); _targetManager.NotifyVoyeurOfEvent(TargetStatus.Teleported); } }