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
|
|
@ -62,6 +62,9 @@ internal sealed class R5Host : IPhysicsObjHost
|
|||
public IPhysicsObjHost? GetObjectA(uint id)
|
||||
=> World.TryGetValue(id, out var h) && h.Resolvable ? h : null;
|
||||
|
||||
public IPhysicsObjHost? GetRelationshipTarget(uint objectId) =>
|
||||
_targetManager?.GetRelationshipTarget(objectId);
|
||||
|
||||
public void HandleUpdateTarget(TargetInfo info)
|
||||
{
|
||||
HandleUpdateTargetCalls.Add(info);
|
||||
|
|
@ -79,12 +82,14 @@ internal sealed class R5Host : IPhysicsObjHost
|
|||
|
||||
public void ClearTarget() => _targetManager?.ClearTarget();
|
||||
|
||||
public void ReceiveTargetUpdate(TargetInfo info) => _targetManager?.ReceiveUpdate(info);
|
||||
public void ReceiveTargetUpdate(TargetInfo info, IPhysicsObjHost sender) =>
|
||||
_targetManager?.ReceiveUpdate(info, sender);
|
||||
|
||||
public void AddVoyeur(uint watcherId, float radius, double quantum)
|
||||
=> TargetManager.AddVoyeur(watcherId, radius, quantum);
|
||||
public void AddVoyeur(IPhysicsObjHost watcher, float radius, double quantum)
|
||||
=> TargetManager.AddVoyeur(watcher, radius, quantum);
|
||||
|
||||
public void RemoveVoyeur(uint watcherId) => _targetManager?.RemoveVoyeur(watcherId);
|
||||
public void RemoveVoyeur(uint watcherId, IPhysicsObjHost expectedWatcher) =>
|
||||
_targetManager?.RemoveVoyeur(watcherId, expectedWatcher);
|
||||
|
||||
// ── test helpers ───────────────────────────────────────────────────────
|
||||
public void SetOrigin(Vector3 origin)
|
||||
|
|
|
|||
|
|
@ -352,6 +352,8 @@ internal sealed class RemoteChaseHarness
|
|||
public double PhysicsTimerTime => _h.Now;
|
||||
public IPhysicsObjHost? GetObjectA(uint id)
|
||||
=> id == PlayerGuid ? _h._playerHost : null;
|
||||
public IPhysicsObjHost? GetRelationshipTarget(uint objectId)
|
||||
=> GetObjectA(objectId);
|
||||
|
||||
public void HandleUpdateTarget(TargetInfo info)
|
||||
{
|
||||
|
|
@ -373,9 +375,9 @@ internal sealed class RemoteChaseHarness
|
|||
}
|
||||
|
||||
public void ClearTarget() => _h._targetArmed = false;
|
||||
public void ReceiveTargetUpdate(TargetInfo info) { }
|
||||
public void AddVoyeur(uint watcherId, float radius, double quantum) { }
|
||||
public void RemoveVoyeur(uint watcherId) { }
|
||||
public void ReceiveTargetUpdate(TargetInfo info, IPhysicsObjHost sender) { }
|
||||
public void AddVoyeur(IPhysicsObjHost watcher, float radius, double quantum) { }
|
||||
public void RemoveVoyeur(uint watcherId, IPhysicsObjHost expectedWatcher) { }
|
||||
}
|
||||
|
||||
private sealed class TargetHost : IPhysicsObjHost
|
||||
|
|
@ -391,13 +393,14 @@ internal sealed class RemoteChaseHarness
|
|||
public double CurTime => _h.Now;
|
||||
public double PhysicsTimerTime => _h.Now;
|
||||
public IPhysicsObjHost? GetObjectA(uint id) => null;
|
||||
public IPhysicsObjHost? GetRelationshipTarget(uint objectId) => null;
|
||||
public void HandleUpdateTarget(TargetInfo info) { }
|
||||
public void InterruptCurrentMovement() { }
|
||||
public void SetTarget(uint contextId, uint objectId, float radius, double quantum) { }
|
||||
public void ClearTarget() { }
|
||||
public void ReceiveTargetUpdate(TargetInfo info) { }
|
||||
public void AddVoyeur(uint watcherId, float radius, double quantum) { }
|
||||
public void RemoveVoyeur(uint watcherId) { }
|
||||
public void ReceiveTargetUpdate(TargetInfo info, IPhysicsObjHost sender) { }
|
||||
public void AddVoyeur(IPhysicsObjHost watcher, float radius, double quantum) { }
|
||||
public void RemoveVoyeur(uint watcherId, IPhysicsObjHost expectedWatcher) { }
|
||||
}
|
||||
|
||||
// ── The per-tick pipeline (GameWindow.TickAnimations order) ────────────
|
||||
|
|
|
|||
|
|
@ -218,11 +218,11 @@ public sealed class StickyManagerTests
|
|||
var self = new R5Host(10u, world) { Radius = 0.5f, MinterpMaxSpeed = 1.0f };
|
||||
var target = new R5Host(20u, world);
|
||||
var sticky = new StickyManager(self);
|
||||
target.Resolvable = false;
|
||||
sticky.StickTo(target.Id, 0.5f, 1.0f);
|
||||
// Cache a position via HandleUpdateTarget, then make the target vanish.
|
||||
// Cache a position without ever establishing an exact target token.
|
||||
var tp = new Position(1u, new Vector3(4f, 0f, 0f), Quaternion.Identity);
|
||||
sticky.HandleUpdateTarget(new TargetInfo(target.Id, TargetStatus.Ok, tp, tp));
|
||||
target.Resolvable = false; // GetObjectA(target) → null → fall back to cached
|
||||
|
||||
var frame = new MotionDeltaFrame();
|
||||
sticky.AdjustOffset(frame, quantum: 0.1);
|
||||
|
|
|
|||
|
|
@ -76,7 +76,9 @@ public sealed class TargetManagerTests
|
|||
int before = self.HandleUpdateTargetCalls.Count;
|
||||
|
||||
var p = new Position(1u, Vector3.Zero, Quaternion.Identity);
|
||||
self.TargetManager.ReceiveUpdate(new TargetInfo(999u, TargetStatus.Ok, p, p));
|
||||
self.TargetManager.ReceiveUpdate(
|
||||
new TargetInfo(999u, TargetStatus.Ok, p, p),
|
||||
target);
|
||||
|
||||
Assert.Equal(before, self.HandleUpdateTargetCalls.Count);
|
||||
}
|
||||
|
|
@ -88,7 +90,9 @@ public sealed class TargetManagerTests
|
|||
self.TargetManager.SetTarget(0, target.Id, 1.0f, 0.0);
|
||||
|
||||
var p = new Position(1u, Vector3.Zero, Quaternion.Identity);
|
||||
self.TargetManager.ReceiveUpdate(new TargetInfo(target.Id, TargetStatus.ExitWorld, p, p));
|
||||
self.TargetManager.ReceiveUpdate(
|
||||
new TargetInfo(target.Id, TargetStatus.ExitWorld, p, p),
|
||||
target);
|
||||
|
||||
Assert.Null(self.TargetManager.TargetInfo); // cleared
|
||||
Assert.False(target.TargetManager.VoyeurTable!.ContainsKey(self.Id)); // unsubscribed
|
||||
|
|
@ -102,7 +106,9 @@ public sealed class TargetManagerTests
|
|||
self.TargetManager.SetTarget(0, target.Id, 1.0f, 0.0);
|
||||
|
||||
var tp = new Position(1u, new Vector3(0f, 5f, 0f), Quaternion.Identity);
|
||||
self.TargetManager.ReceiveUpdate(new TargetInfo(target.Id, TargetStatus.Ok, tp, tp));
|
||||
self.TargetManager.ReceiveUpdate(
|
||||
new TargetInfo(target.Id, TargetStatus.Ok, tp, tp),
|
||||
target);
|
||||
|
||||
// self→target interp position (0,5,0) normalized = +Y.
|
||||
var heading = self.TargetManager.TargetInfo!.Value.InterpolatedHeading;
|
||||
|
|
@ -110,6 +116,46 @@ public sealed class TargetManagerTests
|
|||
Assert.Equal(1f, heading.Y, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReceiveUpdate_SameGuidReplacementSender_IsIgnored()
|
||||
{
|
||||
var (self, originalTarget, world) = TwoHosts();
|
||||
self.TargetManager.SetTarget(0, originalTarget.Id, 1.0f, 0.0);
|
||||
self.HandleUpdateTargetCalls.Clear();
|
||||
var replacement = new R5Host(originalTarget.Id, world);
|
||||
var p = new Position(1u, new Vector3(99f, 0f, 0f), Quaternion.Identity);
|
||||
|
||||
self.ReceiveTargetUpdate(
|
||||
new TargetInfo(originalTarget.Id, TargetStatus.ExitWorld, p, p),
|
||||
replacement);
|
||||
|
||||
Assert.Empty(self.HandleUpdateTargetCalls);
|
||||
Assert.NotNull(self.TargetManager.TargetInfo);
|
||||
Assert.Same(
|
||||
originalTarget,
|
||||
self.TargetManager.GetRelationshipTarget(originalTarget.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StickyAdjustOffset_UsesExactTargetInsteadOfSameGuidReplacement()
|
||||
{
|
||||
var (self, originalTarget, world) = TwoHosts();
|
||||
self.SetOrigin(Vector3.Zero);
|
||||
originalTarget.SetOrigin(new Vector3(5f, 0f, 0f));
|
||||
self.PositionManager.StickTo(originalTarget.Id, 0.5f, 1f);
|
||||
var replacement = new R5Host(originalTarget.Id, world);
|
||||
replacement.SetOrigin(new Vector3(10f, 0f, 0f));
|
||||
originalTarget.SetOrigin(new Vector3(-5f, 0f, 0f));
|
||||
var delta = new MotionDeltaFrame();
|
||||
|
||||
self.PositionManager.AdjustOffset(delta, 0.1);
|
||||
|
||||
Assert.True(delta.Origin.X < 0f);
|
||||
Assert.Same(
|
||||
originalTarget,
|
||||
self.TargetManager.GetRelationshipTarget(originalTarget.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandleTargetting_UndefinedTarget_TimesOutAfter10Seconds()
|
||||
{
|
||||
|
|
@ -133,9 +179,8 @@ public sealed class TargetManagerTests
|
|||
public void HandleTargetting_Throttles_Within500ms()
|
||||
{
|
||||
var (self, target, _) = TwoHosts();
|
||||
var w = new TargettedVoyeurInfo(self.Id, radius: 0.1f, quantum: 0.0);
|
||||
// seed a voyeur directly + advance so a sweep WOULD send if not throttled.
|
||||
target.TargetManager.AddVoyeur(self.Id, 0.1f, 0.0);
|
||||
target.TargetManager.AddVoyeur(self, 0.1f, 0.0);
|
||||
target.SetOrigin(new Vector3(10f, 0f, 0f)); // far past radius
|
||||
|
||||
target.PhysicsTimerTime = 0.3; // < 0.5 throttle
|
||||
|
|
@ -186,12 +231,12 @@ public sealed class TargetManagerTests
|
|||
{
|
||||
var (self, target, _) = TwoHosts();
|
||||
target.SetOrigin(Vector3.Zero);
|
||||
target.TargetManager.AddVoyeur(self.Id, radius: 1.0f, quantum: 0.0);
|
||||
target.TargetManager.AddVoyeur(self, radius: 1.0f, quantum: 0.0);
|
||||
var voyeur = target.TargetManager.VoyeurTable![self.Id];
|
||||
Assert.Equal(Vector3.Zero, voyeur.LastSentPosition.Frame.Origin);
|
||||
|
||||
target.SetOrigin(new Vector3(5f, 0f, 0f));
|
||||
target.TargetManager.AddVoyeur(self.Id, radius: 2.5f, quantum: 0.3); // existing → update only
|
||||
target.TargetManager.AddVoyeur(self, radius: 2.5f, quantum: 0.3); // existing → update only
|
||||
|
||||
Assert.Equal(2.5f, voyeur.Radius);
|
||||
Assert.Equal(0.3, voyeur.Quantum);
|
||||
|
|
@ -202,10 +247,27 @@ public sealed class TargetManagerTests
|
|||
public void RemoveVoyeur_RemovesEntry()
|
||||
{
|
||||
var (self, target, _) = TwoHosts();
|
||||
target.TargetManager.AddVoyeur(self.Id, 1.0f, 0.0);
|
||||
Assert.True(target.TargetManager.RemoveVoyeur(self.Id));
|
||||
target.TargetManager.AddVoyeur(self, 1.0f, 0.0);
|
||||
Assert.True(target.TargetManager.RemoveVoyeur(self.Id, self));
|
||||
Assert.False(target.TargetManager.VoyeurTable!.ContainsKey(self.Id));
|
||||
Assert.False(target.TargetManager.RemoveVoyeur(self.Id)); // already gone
|
||||
Assert.False(target.TargetManager.RemoveVoyeur(self.Id, self)); // already gone
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HiddenWatcher_GuidReuseCannotRedirectOrRemoveExactSubscription()
|
||||
{
|
||||
var (watcher, target, world) = TwoHosts();
|
||||
watcher.TargetManager.SetTarget(0, target.Id, 1.0f, 0.0);
|
||||
watcher.HandleUpdateTargetCalls.Clear();
|
||||
watcher.Resolvable = false;
|
||||
var replacement = new R5Host(watcher.Id, world);
|
||||
|
||||
target.TargetManager.NotifyVoyeurOfEvent(TargetStatus.Teleported);
|
||||
|
||||
Assert.Single(watcher.HandleUpdateTargetCalls);
|
||||
Assert.Empty(replacement.HandleUpdateTargetCalls);
|
||||
Assert.False(target.TargetManager.RemoveVoyeur(watcher.Id, replacement));
|
||||
Assert.True(target.TargetManager.RemoveVoyeur(watcher.Id, watcher));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue