using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace AcDream.Core.Physics.Motion;
///
/// R5 — port of retail's TargetManager (acclient.h:31024, struct #3484;
/// decomp 0x0051a370-0x0051ad90, r5-targetmanager-decomp.md). A
/// peer-to-peer voyeur subscription system with two roles per object:
///
///
/// - Watcher ():
/// registers this object as a voyeur ON a target; the target's per-tick
/// pushes position updates back here via
/// , which fans them to the owning host's
/// MoveToManager + PositionManager (sticky) through
/// .
/// - Watched (): other objects'
/// subscribe to THIS object; each tick
/// → sends a
/// dead-reckoned update to any subscriber the object has drifted past the
/// subscriber's radius from.
///
///
/// This REPLACES the AP-79 minimal TargetTracker adapter (GameWindow
/// polling the entity table). It is a faithful superset: the same
/// move-to tracking (distance > radius → HandleUpdateTarget(Ok)) plus the
/// correct sticky, 10 s timeout, and exit/teleport event handling.
///
/// Owned 1:1 by an (retail
/// CPhysicsObj::target_manager, lazily created on first
/// set_target/add_voyeur). The two throttle constants
/// (=0.5, =10) are
/// ACE's, verified against the retail x87 mush — port-plan §2d.
///
public sealed class TargetManager
{
/// Retail HandleTargetting per-tick throttle (ACE: 0.5s) —
/// the voyeur sweep runs at most this often.
public const double ThrottleSeconds = 0.5;
/// Retail target-info staleness timeout (ACE: 10.0s) — an
/// Undefined-status target with no update for this long is marked
/// TimedOut.
public const double StalenessSeconds = 10.0;
private readonly IPhysicsObjHost _host;
private TargetInfo? _targetInfo; // retail target_info (watcher role)
private Dictionary? _voyeurTable; // retail voyeur_table (watched role)
private double _lastUpdateTime; // retail last_update_time (throttle base)
public TargetManager(IPhysicsObjHost host)
=> _host = host ?? throw new ArgumentNullException(nameof(host));
/// The current watched-target info, or null when tracking
/// nothing.
public TargetInfo? TargetInfo => _targetInfo;
/// The subscriber table (null until the first
/// ).
public IReadOnlyDictionary? VoyeurTable => _voyeurTable;
/// Retail get_target_quantum — the current target's
/// quantum, 0 when tracking nothing.
public double GetTargetQuantum() => _targetInfo?.Quantum ?? 0.0;
// ── watcher role ───────────────────────────────────────────────────────
///
/// Retail TargetManager::SetTarget (0x0051ac30). Tear down any
/// existing target, then: if is 0, synthesize a
/// TimedOut clear-update to the host and leave
/// null; otherwise create a fresh (status
/// Undefined) and subscribe as a voyeur ON the target
/// (target.add_voyeur(self.id, radius, quantum)). The target's live
/// position arrives asynchronously via .
///
public void SetTarget(uint contextId, uint objectId, float radius, double quantum)
{
ClearTarget();
if (objectId == 0)
{
// Clear/cancel: report a TimedOut update carrying the context,
// leave _targetInfo null. (Retail var_10_1 = 6 = TimedOut.)
var cleared = new TargetInfo(
ObjectId: 0, Status: TargetStatus.TimedOut,
TargetPosition: default, InterpolatedPosition: default,
ContextId: contextId);
_host.HandleUpdateTarget(cleared);
return;
}
_targetInfo = new TargetInfo(
ObjectId: objectId, Status: TargetStatus.Undefined,
TargetPosition: default, InterpolatedPosition: default,
ContextId: contextId, Radius: radius, Quantum: quantum,
LastUpdateTime: _host.CurTime);
var target = _host.GetObjectA(objectId);
target?.AddVoyeur(_host.Id, radius, quantum);
}
///
/// Retail TargetManager::SetTargetQuantum (0x0051a4a0). Update the
/// current target's resend interval and re-register the voyeur subscription
/// on the target with the new quantum.
///
public void SetTargetQuantum(double quantum)
{
if (_targetInfo is not { } ti)
return;
_targetInfo = ti with { Quantum = quantum };
var target = _host.GetObjectA(ti.ObjectId);
target?.AddVoyeur(_host.Id, ti.Radius, quantum);
}
///
/// Retail TargetManager::ClearTarget (0x0051a7e0). Unsubscribe from
/// the current target's voyeur table and drop .
///
public void ClearTarget()
{
if (_targetInfo is not { } ti)
return;
var target = _host.GetObjectA(ti.ObjectId);
target?.RemoveVoyeur(_host.Id);
_targetInfo = null;
}
///
/// Retail TargetManager::ReceiveUpdate (0x0051a930). The inbound
/// handler when a target we watch sends us its position (via
/// SendVoyeurUpdate → receive_target_update). Ignores updates
/// for anything but our current target. Copies the payload, stamps receipt
/// time, recomputes the self→target interpolated heading (falls back to +Z
/// when degenerate), fans the snapshot to the host, and drops the
/// subscription on an ExitWorld status.
///
public void ReceiveUpdate(TargetInfo update)
{
if (_targetInfo is not { } ti || ti.ObjectId != update.ObjectId)
return;
// Copy radius/quantum/positions/velocity/status from the wire; keep our
// object_id; stamp receipt time.
Vector3 interpHeading = update.InterpolatedPosition.Frame.Origin
- _host.Position.Frame.Origin;
if (MoveToMath.NormalizeCheckSmall(ref interpHeading))
interpHeading = Vector3.UnitZ;
var updated = ti with
{
Radius = update.Radius,
Quantum = update.Quantum,
TargetPosition = update.TargetPosition,
InterpolatedPosition = update.InterpolatedPosition,
Velocity = update.Velocity,
Status = update.Status,
InterpolatedHeading = interpHeading,
LastUpdateTime = _host.CurTime,
};
_targetInfo = updated;
_host.HandleUpdateTarget(updated);
if (update.Status == TargetStatus.ExitWorld)
ClearTarget();
}
// ── watched role ───────────────────────────────────────────────────────
///
/// Retail TargetManager::AddVoyeur (0x0051a830). A subscriber
/// registers to watch this object. If already subscribed, updates its
/// radius/quantum in place (no immediate send); otherwise creates the entry
/// and pushes an immediate initial snapshot (Ok).
///
public void AddVoyeur(uint watcherId, float radius, double quantum)
{
_voyeurTable ??= new Dictionary();
if (_voyeurTable.TryGetValue(watcherId, out var existing))
{
existing.Radius = radius;
existing.Quantum = quantum;
return;
}
var voyeur = new TargettedVoyeurInfo(watcherId, radius, quantum);
_voyeurTable[watcherId] = voyeur;
SendVoyeurUpdate(voyeur, _host.Position, TargetStatus.Ok);
}
/// Retail TargetManager::RemoveVoyeur (0x0051ad90).
public bool RemoveVoyeur(uint watcherId)
=> _voyeurTable?.Remove(watcherId) ?? false;
///
/// Retail TargetManager::HandleTargetting (0x0051aa90). THE per-tick
/// driver (no separate UseTime): self-throttled to
/// , promotes a stale target to TimedOut after
/// , then sweeps every subscriber through
/// .
///
public void HandleTargetting()
{
if (_host.PhysicsTimerTime - _lastUpdateTime < ThrottleSeconds)
return;
if (_targetInfo is { } ti)
{
if (ti.Status == TargetStatus.Undefined
&& ti.LastUpdateTime + StalenessSeconds < _host.CurTime)
{
var timedOut = ti with { Status = TargetStatus.TimedOut };
_targetInfo = timedOut;
_host.HandleUpdateTarget(timedOut);
}
}
if (_voyeurTable != null)
{
foreach (var voyeur in _voyeurTable.Values.ToList())
CheckAndUpdateVoyeur(voyeur);
}
_lastUpdateTime = _host.PhysicsTimerTime;
}
///
/// Retail TargetManager::CheckAndUpdateVoyeur (0x0051a650). Push an
/// update to only if this object's dead-reckoned
/// position has drifted more than the voyeur's radius since the last send.
///
public void CheckAndUpdateVoyeur(TargettedVoyeurInfo voyeur)
{
Position newPos = GetInterpolatedPosition(voyeur.Quantum);
float drift = Vector3.Distance(
newPos.Frame.Origin, voyeur.LastSentPosition.Frame.Origin);
if (drift > voyeur.Radius)
SendVoyeurUpdate(voyeur, newPos, TargetStatus.Ok);
}
///
/// Retail TargetManager::GetInterpolatedPosition (0x0051a5e0).
/// Dead-reckon this object's position forward by
/// seconds using its current velocity.
///
public Position GetInterpolatedPosition(double quantum)
{
var pos = _host.Position;
Vector3 origin = pos.Frame.Origin + _host.Velocity * (float)quantum;
return new Position(pos.ObjCellId, origin, pos.Frame.Orientation);
}
///
/// Retail TargetManager::SendVoyeurUpdate (0x0051a4f0). Record the
/// sent position on the voyeur, build a carrying
/// this object's CURRENT authoritative position + the extrapolated
/// + velocity + status, and deliver it to the
/// subscriber's (via its host).
///
public void SendVoyeurUpdate(TargettedVoyeurInfo voyeur, Position pos, TargetStatus status)
{
voyeur.LastSentPosition = pos;
var info = new TargetInfo(
ObjectId: _host.Id,
Status: status,
TargetPosition: _host.Position, // current authoritative
InterpolatedPosition: pos, // the extrapolated position
ContextId: 0,
Radius: voyeur.Radius,
Quantum: voyeur.Quantum,
Velocity: _host.Velocity);
var voyeurObj = _host.GetObjectA(voyeur.ObjectId);
voyeurObj?.ReceiveTargetUpdate(info);
}
///
/// Retail TargetManager::NotifyVoyeurOfEvent (0x0051a6f0). Broadcast
/// a discrete status event (e.g. ExitWorld, Teleported) to every subscriber
/// with this object's CURRENT position — no distance gate.
///
public void NotifyVoyeurOfEvent(TargetStatus status)
{
if (_voyeurTable == null)
return;
foreach (var voyeur in _voyeurTable.Values.ToList())
SendVoyeurUpdate(voyeur, _host.Position, status);
}
}