refactor(world): canonicalize live physics host ownership
This commit is contained in:
parent
5882b308c1
commit
fcb66198fc
21 changed files with 1546 additions and 323 deletions
|
|
@ -16,9 +16,9 @@ namespace AcDream.Core.Physics.Motion;
|
|||
/// <c>RemoteMotion</c> or the local player), wiring the accessors to the live
|
||||
/// <see cref="PhysicsBody"/> and the <see cref="MoveToManager"/> /
|
||||
/// <see cref="PositionManager"/> / <see cref="TargetManager"/> it owns.
|
||||
/// <see cref="GetObjectA"/> is backed by the App's live entity table
|
||||
/// (<c>_entitiesByServerGuid</c>), giving the voyeur round-trip its
|
||||
/// cross-entity delivery path.</para>
|
||||
/// <see cref="GetObjectA"/> is backed by the App's canonical
|
||||
/// <c>LiveEntityRuntime</c> records, giving the voyeur round-trip its
|
||||
/// cross-entity delivery path without a second GUID index.</para>
|
||||
/// </summary>
|
||||
public interface IPhysicsObjHost
|
||||
{
|
||||
|
|
@ -57,11 +57,21 @@ public interface IPhysicsObjHost
|
|||
double PhysicsTimerTime { get; }
|
||||
|
||||
/// <summary>Retail <c>CObjectMaint::GetObjectA(id)</c> — resolve another
|
||||
/// physics object by guid, or <c>null</c> if not currently known/visible.
|
||||
/// The cross-entity seam for the voyeur round-trip and sticky live-target
|
||||
/// physics object by guid from the object table. The App additionally
|
||||
/// withholds Hidden objects from ordinary relationship creation through
|
||||
/// its registered TS-49 DetectionManager adaptation. This is the
|
||||
/// cross-entity seam for the voyeur round-trip and sticky live-target
|
||||
/// resolve.</summary>
|
||||
IPhysicsObjHost? GetObjectA(uint id);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the exact target incarnation captured by this host's current
|
||||
/// TargetManager relationship. App teardown can overlap a newer
|
||||
/// INSTANCE_TS with the same GUID, so StickyManager must not re-resolve the
|
||||
/// relationship through the active GUID table.
|
||||
/// </summary>
|
||||
IPhysicsObjHost? GetRelationshipTarget(uint objectId);
|
||||
|
||||
/// <summary>Retail <c>CPhysicsObj::HandleUpdateTarget</c> — fans a
|
||||
/// <see cref="TargetInfo"/> to this host's <see cref="MoveToManager"/>
|
||||
/// (move-to steering) AND <see cref="PositionManager"/> (sticky follow).
|
||||
|
|
@ -86,15 +96,19 @@ public interface IPhysicsObjHost
|
|||
/// <summary>Retail <c>CPhysicsObj::receive_target_update</c> →
|
||||
/// <see cref="TargetManager.ReceiveUpdate"/>. The inbound side a SENDER's
|
||||
/// <c>SendVoyeurUpdate</c> tail-calls on the watcher.</summary>
|
||||
void ReceiveTargetUpdate(TargetInfo info);
|
||||
void ReceiveTargetUpdate(TargetInfo info, IPhysicsObjHost sender);
|
||||
|
||||
/// <summary>Retail <c>CPhysicsObj::add_voyeur(id, radius, quantum)</c> →
|
||||
/// <see cref="TargetManager.AddVoyeur"/> (lazily creating the
|
||||
/// TargetManager). Called on the TARGET when a watcher subscribes.</summary>
|
||||
void AddVoyeur(uint watcherId, float radius, double quantum);
|
||||
/// TargetManager). Called on the TARGET when a watcher subscribes. The
|
||||
/// exact watcher host preserves retail's unique object-table identity
|
||||
/// while App teardown overlaps a newer same-GUID generation.</summary>
|
||||
void AddVoyeur(IPhysicsObjHost watcher, float radius, double quantum);
|
||||
|
||||
/// <summary>Retail <c>CPhysicsObj::remove_voyeur(id)</c> →
|
||||
/// <see cref="TargetManager.RemoveVoyeur"/>. Called on the TARGET when a
|
||||
/// watcher unsubscribes.</summary>
|
||||
void RemoveVoyeur(uint watcherId);
|
||||
/// watcher unsubscribes. The expected watcher identity prevents a retiring
|
||||
/// same-GUID incarnation from removing a newer incarnation's subscription
|
||||
/// while App teardown callbacks converge.</summary>
|
||||
void RemoveVoyeur(uint watcherId, IPhysicsObjHost expectedWatcher);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ public sealed class StickyManager
|
|||
return;
|
||||
|
||||
var self = _host.Position;
|
||||
var target = _host.GetObjectA(TargetId);
|
||||
var target = _host.GetRelationshipTarget(TargetId);
|
||||
var targetPos = target != null ? target.Position : TargetPosition;
|
||||
|
||||
// offset = local-frame, Z-flattened vector from self to target.
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ public sealed class TargetManager
|
|||
private readonly IPhysicsObjHost _host;
|
||||
|
||||
private TargetInfo? _targetInfo; // retail target_info (watcher role)
|
||||
private IPhysicsObjHost? _targetHost; // exact App incarnation token
|
||||
private Dictionary<uint, TargettedVoyeurInfo>? _voyeurTable; // retail voyeur_table (watched role)
|
||||
private double _lastUpdateTime; // retail last_update_time (throttle base)
|
||||
|
||||
|
|
@ -67,6 +68,12 @@ public sealed class TargetManager
|
|||
/// quantum, 0 when tracking nothing.</summary>
|
||||
public double GetTargetQuantum() => _targetInfo?.Quantum ?? 0.0;
|
||||
|
||||
/// <summary>The pointer-like target identity captured by SetTarget.</summary>
|
||||
public IPhysicsObjHost? GetRelationshipTarget(uint objectId) =>
|
||||
_targetInfo is { } info && info.ObjectId == objectId
|
||||
? _targetHost
|
||||
: null;
|
||||
|
||||
// ── watcher role ───────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -101,7 +108,8 @@ public sealed class TargetManager
|
|||
LastUpdateTime: _host.CurTime);
|
||||
|
||||
var target = _host.GetObjectA(objectId);
|
||||
target?.AddVoyeur(_host.Id, radius, quantum);
|
||||
_targetHost = target;
|
||||
target?.AddVoyeur(_host, radius, quantum);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -115,8 +123,9 @@ public sealed class TargetManager
|
|||
return;
|
||||
|
||||
_targetInfo = ti with { Quantum = quantum };
|
||||
var target = _host.GetObjectA(ti.ObjectId);
|
||||
target?.AddVoyeur(_host.Id, ti.Radius, quantum);
|
||||
var target = _targetHost ?? _host.GetObjectA(ti.ObjectId);
|
||||
_targetHost = target;
|
||||
target?.AddVoyeur(_host, ti.Radius, quantum);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -128,9 +137,10 @@ public sealed class TargetManager
|
|||
if (_targetInfo is not { } ti)
|
||||
return;
|
||||
|
||||
var target = _host.GetObjectA(ti.ObjectId);
|
||||
target?.RemoveVoyeur(_host.Id);
|
||||
var target = _targetHost ?? _host.GetObjectA(ti.ObjectId);
|
||||
target?.RemoveVoyeur(_host.Id, _host);
|
||||
_targetInfo = null;
|
||||
_targetHost = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -141,10 +151,17 @@ public sealed class TargetManager
|
|||
/// 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.
|
||||
/// Receives the update only from the exact target incarnation that
|
||||
/// established this relationship. The sender token stays outside the
|
||||
/// retail TargetInfo payload.
|
||||
/// </summary>
|
||||
public void ReceiveUpdate(TargetInfo update)
|
||||
public void ReceiveUpdate(TargetInfo update, IPhysicsObjHost sender)
|
||||
{
|
||||
if (_targetInfo is not { } ti || ti.ObjectId != update.ObjectId)
|
||||
ArgumentNullException.ThrowIfNull(sender);
|
||||
if (_targetInfo is not { } ti
|
||||
|| ti.ObjectId != update.ObjectId
|
||||
|| _targetHost is null
|
||||
|| !ReferenceEquals(_targetHost, sender))
|
||||
return;
|
||||
|
||||
// Copy radius/quantum/positions/velocity/status from the wire; keep our
|
||||
|
|
@ -181,25 +198,53 @@ public sealed class TargetManager
|
|||
/// radius/quantum in place (no immediate send); otherwise creates the entry
|
||||
/// and pushes an immediate initial snapshot (<c>Ok</c>).
|
||||
/// </summary>
|
||||
public void AddVoyeur(uint watcherId, float radius, double quantum)
|
||||
public void AddVoyeur(IPhysicsObjHost watcher, float radius, double quantum)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(watcher);
|
||||
uint watcherId = watcher.Id;
|
||||
_voyeurTable ??= new Dictionary<uint, TargettedVoyeurInfo>();
|
||||
|
||||
if (_voyeurTable.TryGetValue(watcherId, out var existing))
|
||||
{
|
||||
existing.Radius = radius;
|
||||
existing.Quantum = quantum;
|
||||
return;
|
||||
if (ReferenceEquals(existing.WatcherHost, watcher))
|
||||
{
|
||||
existing.Radius = radius;
|
||||
existing.Quantum = quantum;
|
||||
return;
|
||||
}
|
||||
|
||||
// A later INSTANCE_TS reused the GUID. Replace the pointer-like
|
||||
// subscription; the retiring watcher can no longer remove it via
|
||||
// its exact identity token.
|
||||
_voyeurTable.Remove(watcherId);
|
||||
}
|
||||
|
||||
var voyeur = new TargettedVoyeurInfo(watcherId, radius, quantum);
|
||||
var voyeur = new TargettedVoyeurInfo(
|
||||
watcherId,
|
||||
radius,
|
||||
quantum,
|
||||
watcher);
|
||||
_voyeurTable[watcherId] = voyeur;
|
||||
SendVoyeurUpdate(voyeur, _host.Position, TargetStatus.Ok);
|
||||
}
|
||||
|
||||
/// <summary>Retail <c>TargetManager::RemoveVoyeur</c> (0x0051ad90).</summary>
|
||||
public bool RemoveVoyeur(uint watcherId)
|
||||
=> _voyeurTable?.Remove(watcherId) ?? false;
|
||||
/// <summary>
|
||||
/// Retail <c>TargetManager::RemoveVoyeur</c> (0x0051ad90). Removes a
|
||||
/// relationship only when it still belongs to the exact watcher
|
||||
/// incarnation that established it in the App lifetime model.
|
||||
/// </summary>
|
||||
public bool RemoveVoyeur(uint watcherId, IPhysicsObjHost expectedWatcher)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(expectedWatcher);
|
||||
if (_voyeurTable is null
|
||||
|| !_voyeurTable.TryGetValue(watcherId, out var existing)
|
||||
|| !ReferenceEquals(existing.WatcherHost, expectedWatcher))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _voyeurTable.Remove(watcherId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>TargetManager::HandleTargetting</c> (0x0051aa90). THE per-tick
|
||||
|
|
@ -280,8 +325,7 @@ public sealed class TargetManager
|
|||
Quantum: voyeur.Quantum,
|
||||
Velocity: _host.Velocity);
|
||||
|
||||
var voyeurObj = _host.GetObjectA(voyeur.ObjectId);
|
||||
voyeurObj?.ReceiveTargetUpdate(info);
|
||||
voyeur.WatcherHost.ReceiveTargetUpdate(info, _host);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,14 @@ namespace AcDream.Core.Physics.Motion;
|
|||
/// </summary>
|
||||
public sealed class TargettedVoyeurInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Exact watcher incarnation captured when this subscription is created.
|
||||
/// Retail object IDs are process-lifetime object-table identities; the App
|
||||
/// preserves that pointer-like identity explicitly while an old
|
||||
/// INSTANCE_TS teardown may overlap a newer record with the same GUID.
|
||||
/// </summary>
|
||||
internal IPhysicsObjHost WatcherHost { get; }
|
||||
|
||||
/// <summary>+0x00 retail <c>object_id</c> — the subscriber's guid.</summary>
|
||||
public uint ObjectId { get; }
|
||||
|
||||
|
|
@ -27,10 +35,23 @@ public sealed class TargettedVoyeurInfo
|
|||
/// delivered to this subscriber (updated by <c>SendVoyeurUpdate</c>).</summary>
|
||||
public Position LastSentPosition { get; set; }
|
||||
|
||||
public TargettedVoyeurInfo(uint objectId, float radius, double quantum)
|
||||
public TargettedVoyeurInfo(
|
||||
uint objectId,
|
||||
float radius,
|
||||
double quantum,
|
||||
IPhysicsObjHost watcherHost)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(watcherHost);
|
||||
if (watcherHost.Id != objectId)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Watcher identity must match the voyeur object ID.",
|
||||
nameof(watcherHost));
|
||||
}
|
||||
|
||||
ObjectId = objectId;
|
||||
Radius = radius;
|
||||
Quantum = quantum;
|
||||
WatcherHost = watcherHost;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue