refactor(world): canonicalize live physics host ownership

This commit is contained in:
Erik 2026-07-21 14:05:34 +02:00
parent 5882b308c1
commit fcb66198fc
21 changed files with 1546 additions and 323 deletions

View file

@ -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]