feat(physics): R5-V2 — wire TargetManager voyeur system per-entity, retire AP-79
Replace the AP-79 P4 TargetTracker poll adapter with the ported retail TargetManager voyeur subscription system, wired per entity. Behaviorally a faithful no-op refactor for the common cases (server-directed creature chase + auto-walk-to-object), now driven by the retail mechanism instead of the GameWindow poll. New: EntityPhysicsHost (App) — the per-entity IPhysicsObjHost (retail CPhysicsObj stand-in), owning a TargetManager. Delegate-injected accessors so it stays free of GameWindow internals (code-structure rule #1). Wiring (GameWindow): - _physicsHosts registry (guid → host) = retail CObjectMaint::GetObjectA, backing the voyeur round-trip's cross-entity delivery. - ResolvePhysicsHost lazily creates a minimal position-only host for ANY known entity — retail lets every CPhysicsObj host a target_manager, so a STATIC object (chest/corpse) still answers add_voyeur; without this, auto-walk to a never-animated object would arm the moveto but never receive the immediate target snapshot and never start. - MoveToManager set_target/clear_target/quantum seams repointed at the host's TargetManager (remote + player). - HandleTargetting ticked unconditionally per entity BEFORE UseTime (retail UpdateObjectInternal order): per-remote loop for remotes, pre-Update block for the player. The player's tick is load-bearing for creature-chase — the player as a watched target pushes its position to the chasing NPCs' HandleUpdateTarget each frame, ahead of their UseTime. - Despawn (RemoveLiveEntityByServerGuid): NotifyVoyeurOfEvent(ExitWorld) to the entity's watchers before pruning the host — the only cleanup for a watcher whose target already sent an Ok (past Undefined, the 10s staleness never fires). Deleted: RemoteMotion.TrackedTarget* fields + _playerMoveToTarget* GameWindow fields + the two manual poll blocks. AP-79 register row retired same commit (AP section 73→72). Delivery is now synchronous-on-set_target (retail: set_target is last in MoveToObject, so the immediate snapshot lands with all moveto state in place) vs AP-79's next-frame poll — more faithful. Full suite 4006 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
2b5e8a6738
commit
fffe90b30a
3 changed files with 295 additions and 130 deletions
115
src/AcDream.App/Rendering/EntityPhysicsHost.cs
Normal file
115
src/AcDream.App/Rendering/EntityPhysicsHost.cs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// R5-V2 — the App-side <see cref="IPhysicsObjHost"/> per entity: acdream's
|
||||
/// stand-in for retail's <c>CPhysicsObj</c> as the movement managers see it.
|
||||
/// One is built per entity (a remote <c>RemoteMotion</c> or the local player)
|
||||
/// in <c>GameWindow.EnsureRemoteMotionBindings</c> / <c>EnterPlayerModeNow</c>
|
||||
/// and registered in <c>GameWindow._physicsHosts</c> (guid → host), so
|
||||
/// <see cref="GetObjectA"/> can resolve OTHER entities' hosts — the
|
||||
/// cross-entity delivery path the <see cref="TargetManager"/> voyeur system
|
||||
/// needs.
|
||||
///
|
||||
/// <para>Owns a <see cref="TargetManager"/> (retail
|
||||
/// <c>CPhysicsObj::target_manager</c>). Its <c>set_target</c>/<c>clear_target</c>/
|
||||
/// <c>add_voyeur</c>/<c>remove_voyeur</c>/<c>receive_target_update</c> 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
|
||||
/// <see cref="HandleUpdateTarget"/> fan-out are injected by GameWindow so this
|
||||
/// class stays free of GameWindow's internals (code-structure rule #1).</para>
|
||||
///
|
||||
/// <para>PositionManager (sticky) is R5-V3 — this host gains a
|
||||
/// <c>PositionManager</c> and <see cref="HandleUpdateTarget"/> fans to it then;
|
||||
/// V2 delivers only to the MoveToManager.</para>
|
||||
/// </summary>
|
||||
public sealed class EntityPhysicsHost : IPhysicsObjHost
|
||||
{
|
||||
private readonly Func<Position> _getPosition;
|
||||
private readonly Func<Vector3> _getVelocity;
|
||||
private readonly Func<float> _getRadius;
|
||||
private readonly Func<bool> _inContact;
|
||||
private readonly Func<float?> _minterpMaxSpeed;
|
||||
private readonly Func<double> _curTime;
|
||||
private readonly Func<double> _physicsTimerTime;
|
||||
private readonly Func<uint, IPhysicsObjHost?> _getObjectA;
|
||||
private readonly Action<TargetInfo> _handleUpdateTarget;
|
||||
private readonly Action _interruptCurrentMovement;
|
||||
private readonly TargetManager _targetManager;
|
||||
|
||||
public EntityPhysicsHost(
|
||||
uint id,
|
||||
Func<Position> getPosition,
|
||||
Func<Vector3> getVelocity,
|
||||
Func<float> getRadius,
|
||||
Func<bool> inContact,
|
||||
Func<float?> minterpMaxSpeed,
|
||||
Func<double> curTime,
|
||||
Func<double> physicsTimerTime,
|
||||
Func<uint, IPhysicsObjHost?> getObjectA,
|
||||
Action<TargetInfo> 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);
|
||||
}
|
||||
|
||||
// ── 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();
|
||||
|
||||
/// <summary>The owned voyeur manager (retail
|
||||
/// <c>CPhysicsObj::target_manager</c>).</summary>
|
||||
public TargetManager TargetManager => _targetManager;
|
||||
|
||||
// ── IPhysicsObjHost fan-out / target-tracking seams ────────────────────
|
||||
public IPhysicsObjHost? GetObjectA(uint id) => _getObjectA(id);
|
||||
public void HandleUpdateTarget(TargetInfo info) => _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) ─────────────────
|
||||
|
||||
/// <summary>Retail <c>TargetManager::HandleTargetting</c> — the per-tick
|
||||
/// voyeur sweep (self-throttled to 0.5 s). Retail runs it unconditionally
|
||||
/// for every entity in <c>UpdateObjectInternal</c>, BEFORE the movement
|
||||
/// managers' <c>UseTime</c>.</summary>
|
||||
public void HandleTargetting() => _targetManager.HandleTargetting();
|
||||
|
||||
/// <summary>Retail <c>CPhysicsObj::exit_world</c>'s
|
||||
/// <c>TargetManager::NotifyVoyeurOfEvent(ExitWorld)</c> — 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.</summary>
|
||||
public void NotifyExitWorld() => _targetManager.NotifyVoyeurOfEvent(TargetStatus.ExitWorld);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue