From 0fbc7a1fb74036cb45d02cd42575844f5d47fb5e Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 1 Aug 2026 18:22:45 +0200 Subject: [PATCH] fix(runtime): preserve hidden setposition collision ownership --- .../Physics/ShadowObjectRegistry.cs | 28 +-- .../Entities/RuntimeEntityObjectLifetime.cs | 6 + .../Physics/RuntimeCollisionReportingState.cs | 20 ++ .../Physics/RuntimePhysicsState.cs | 3 +- .../Physics/RuntimeSetPositionState.cs | 30 ++- .../Physics/ShadowSetPositionCommitTests.cs | 27 +++ .../RuntimeCollisionReportingStateTests.cs | 227 +++++++++++++++++- .../Physics/RuntimeSetPositionStateTests.cs | 45 ++++ 8 files changed, 343 insertions(+), 43 deletions(-) diff --git a/src/AcDream.Core/Physics/ShadowObjectRegistry.cs b/src/AcDream.Core/Physics/ShadowObjectRegistry.cs index 083011ed..df86dbd4 100644 --- a/src/AcDream.Core/Physics/ShadowObjectRegistry.cs +++ b/src/AcDream.Core/Physics/ShadowObjectRegistry.cs @@ -883,23 +883,17 @@ public sealed class ShadowObjectRegistry DataCache = DataCache, }; staging.InstallOwnerState(source); - if (suspendOwner) - { - if (!staging.Suspend(entityId)) - return false; - } - else - { - staging.CommitSetPosition( - entityId, - worldPosition, - worldRotation, - seedCellId, - worldOffsetX, - worldOffsetY, - action, - crossCellIds); - } + staging.CommitSetPosition( + entityId, + worldPosition, + worldRotation, + seedCellId, + worldOffsetX, + worldOffsetY, + action, + crossCellIds); + if (suspendOwner && !staging.Suspend(entityId)) + return false; if (!staging.TryCaptureOwnerState( entityId, out PreparedShadowOwnerState? replacement) diff --git a/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs b/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs index 3814f099..03dd3cc1 100644 --- a/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs +++ b/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs @@ -752,6 +752,12 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable transition = Entities.ApplyRawPhysicsState( canonical, update.PhysicsState); + if (canonical.Key is { } key) + { + Physics.Engine.ShadowObjects.UpdatePhysicsState( + key.LocalEntityId, + (uint)canonical.FinalPhysicsState); + } ulong stateVersion = canonical.StateAuthorityVersion; ulong physicsMutationVersion = canonical.PhysicsStateMutationVersion; diff --git a/src/AcDream.Runtime/Physics/RuntimeCollisionReportingState.cs b/src/AcDream.Runtime/Physics/RuntimeCollisionReportingState.cs index b3dbc360..bfa13331 100644 --- a/src/AcDream.Runtime/Physics/RuntimeCollisionReportingState.cs +++ b/src/AcDream.Runtime/Physics/RuntimeCollisionReportingState.cs @@ -438,6 +438,12 @@ internal sealed class RuntimeCollisionReportingState : IDisposable bool reported = false; for (int index = 0; index < receipt.Actions.Length; index++) { + if (IsExactSetPositionBatchOwnerHidden(receipt)) + { + return new( + SetPositionCollisionBatchDispatchStatus.Completed, + reported); + } if (receipt.Owner.PositionAuthorityVersion != receipt.OwnerPositionAuthorityVersion || _owners.TryGetValue( @@ -463,6 +469,12 @@ internal sealed class RuntimeCollisionReportingState : IDisposable action, receipt.PhysicsTime, receipt.BatchId); } + if (IsExactSetPositionBatchOwnerHidden(receipt)) + { + return new( + SetPositionCollisionBatchDispatchStatus.Completed, + reported); + } if (receipt.Owner.PositionAuthorityVersion != receipt.OwnerPositionAuthorityVersion || _owners.TryGetValue( @@ -498,6 +510,14 @@ internal sealed class RuntimeCollisionReportingState : IDisposable reported); } + private bool IsExactSetPositionBatchOwnerHidden( + in SetPositionCollisionBatchReceipt receipt) => + _entities.IsCurrent(receipt.Owner) + && receipt.Owner.PositionAuthorityVersion + == receipt.OwnerPositionAuthorityVersion + && ReferenceEquals(receipt.Owner.PhysicsBody, receipt.OwnerBody) + && (receipt.OwnerBody.State & PhysicsStateFlags.Hidden) != 0; + internal bool DiscardSetPositionBatch( in SetPositionCollisionBatchReceipt receipt) => receipt.IsValid diff --git a/src/AcDream.Runtime/Physics/RuntimePhysicsState.cs b/src/AcDream.Runtime/Physics/RuntimePhysicsState.cs index 86ab4deb..52f12009 100644 --- a/src/AcDream.Runtime/Physics/RuntimePhysicsState.cs +++ b/src/AcDream.Runtime/Physics/RuntimePhysicsState.cs @@ -3199,8 +3199,7 @@ public sealed class RuntimePhysicsState : IDisposable && record.PositionAuthorityVersion == positionAuthorityVersion && record.SpatialAuthorityVersion == spatialAuthorityVersion && ReferenceEquals(record.PhysicsBody, body) - && body.InWorld - && (body.State & PhysicsStateFlags.Hidden) == 0; + && body.InWorld; } private void OnCollisionOwnerMutated(uint ownerId, ulong version) diff --git a/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs b/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs index b817d45d..86aae1d4 100644 --- a/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs +++ b/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs @@ -392,8 +392,7 @@ internal sealed class RuntimeSetPositionState : IDisposable body, placementCommitVersion, fullCellId, - requireSpatialRoot: false) - && owner.IsCollisionReportingEligible(record, body); + requireSpatialRoot: false); } private readonly RuntimePhysicsState _physics; @@ -3389,21 +3388,21 @@ internal sealed class RuntimeSetPositionState : IDisposable body, canonicalCommitVersion, committedCellId, - requireSpatialRoot: false) - || !IsCollisionReportingEligible(record, body)) + requireSpatialRoot: false)) { return false; } - bool reportingCurrent = _physics.HandleSetPositionCollisionReports( - record, - operation.PositionAuthorityVersion, - operation.SpatialAuthorityVersion, - operation.Command.GameTime, - operation.PreviousContact, - operation.PreviousOnWalkable, - collidedWithEnvironment, - collidedObjectIds, - out _); + bool reportingCurrent = !IsCollisionReportingEligible(record, body) + || _physics.HandleSetPositionCollisionReports( + record, + operation.PositionAuthorityVersion, + operation.SpatialAuthorityVersion, + operation.Command.GameTime, + operation.PreviousContact, + operation.PreviousOnWalkable, + collidedWithEnvironment, + collidedObjectIds, + out _); if (!reportingCurrent || !IsCanonicalPlacementCommitCurrent( operation, @@ -3411,8 +3410,7 @@ internal sealed class RuntimeSetPositionState : IDisposable body, canonicalCommitVersion, committedCellId, - requireSpatialRoot: false) - || !IsCollisionReportingEligible(record, body)) + requireSpatialRoot: false)) return false; body.FramesStationaryFall = result.FramesStationaryFall; if (IsVelocityCurrent(operation)) diff --git a/tests/AcDream.Core.Tests/Physics/ShadowSetPositionCommitTests.cs b/tests/AcDream.Core.Tests/Physics/ShadowSetPositionCommitTests.cs index 2b3d8a51..9c8136ba 100644 --- a/tests/AcDream.Core.Tests/Physics/ShadowSetPositionCommitTests.cs +++ b/tests/AcDream.Core.Tests/Physics/ShadowSetPositionCommitTests.cs @@ -38,6 +38,33 @@ public sealed class ShadowSetPositionCommitTests Assert.Single(registry.GetObjectsInCell(Cell9)).Position); } + [Fact] + public void PreparedSuspendedMoveRetainsNewCanonicalPosition() + { + var registry = RegisteredSingle(); + var moved = new Vector3(36f, 12f, 50f); + Assert.True(registry.TryPrepareSetPosition( + 1u, + moved, + Quaternion.Identity, + Cell9, + 0f, + 0f, + PhysicsShadowCommitAction.Replace, + [Cell9], + provenShapeless: false, + suspendOwner: true, + out var prepared)); + + Assert.Single(registry.GetObjectsInCell(Cell1)); + Assert.True(registry.TryApplySetPosition(prepared!, out _)); + Assert.Empty(registry.GetObjectsInCell(Cell1)); + Assert.Empty(registry.GetObjectsInCell(Cell9)); + Assert.Equal(1, registry.SuspendedRegistrationCount); + Assert.Equal(moved, prepared!.OwnerState!.Registration.EntityWorldPos); + Assert.Equal(Cell9, prepared.OwnerState.Registration.SeedCellId); + } + [Fact] public void PreparedCrossPrefixDispatchesMembershipThenMutationOnce() { diff --git a/tests/AcDream.Runtime.Tests/Physics/RuntimeCollisionReportingStateTests.cs b/tests/AcDream.Runtime.Tests/Physics/RuntimeCollisionReportingStateTests.cs index efabd2e4..f23ea6f7 100644 --- a/tests/AcDream.Runtime.Tests/Physics/RuntimeCollisionReportingStateTests.cs +++ b/tests/AcDream.Runtime.Tests/Physics/RuntimeCollisionReportingStateTests.cs @@ -1883,7 +1883,7 @@ public sealed class RuntimeCollisionReportingStateTests } [Fact] - public void HitGroundHiddenTransitionCannotResumeCollisionTracking() + public void HitGroundHiddenTransitionCommitsPlacementWithoutResumingCollisionTracking() { using var lifetime = Lifetime(); RuntimeEntityRecord owner = Entity( @@ -1937,11 +1937,20 @@ public sealed class RuntimeCollisionReportingStateTests PlacementCommand(owner, new Vector3(16f, 12f, 7f))); Assert.True(hidden); - Assert.Equal(RuntimeSetPositionStatus.Cancelled, outcome.Status); + Assert.Equal( + RuntimeSetPositionStatus.CommittedHostAcknowledgementPending, + outcome.Status); + Assert.True(lifetime.Physics.SetPosition.TryPeekProjection( + out RuntimePlacementProjectionSnapshot projection)); Assert.True(owner.FinalPhysicsState.HasFlag(PhysicsStateFlags.Hidden)); + Assert.Equal(Cell, owner.FullCellId); + Assert.Equal(projection.WorldPosition, owner.PhysicsBody!.Position); + Assert.True(owner.PhysicsBody.InWorld); Assert.Empty(observer.Reports); Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership() .TrackedObjectCount); + Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection( + outcome.Projection)); } [Fact] @@ -2010,7 +2019,7 @@ public sealed class RuntimeCollisionReportingStateTests } [Fact] - public void CollisionReportHiddenTransitionStopsResponseAndShadowReflood() + public void CollisionReportHiddenTransitionPreservesCommittedPlacement() { using var lifetime = Lifetime(); RuntimeEntityRecord owner = Entity( @@ -2023,7 +2032,6 @@ public sealed class RuntimeCollisionReportingStateTests RegisterDynamicShadow(lifetime, owner); RegisterDynamicShadow(lifetime, target); uint targetId = target.Key!.Value.LocalEntityId; - Vector3 initial = owner.PhysicsBody!.Position; lifetime.Physics.Engine.TransitionCellCollisionTestHook = (transition, phase, _, _) => { @@ -2062,14 +2070,217 @@ public sealed class RuntimeCollisionReportingStateTests PlacementCommand(owner, new Vector3(18f, 12f, 7f))); Assert.True(hidden); - Assert.Equal(RuntimeSetPositionStatus.Cancelled, outcome.Status); + Assert.Equal( + RuntimeSetPositionStatus.CommittedHostAcknowledgementPending, + outcome.Status); Assert.True(owner.FinalPhysicsState.HasFlag(PhysicsStateFlags.Hidden)); Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership() .TrackedObjectCount); - ShadowEntry shadow = Assert.Single( - lifetime.Physics.Engine.ShadowObjects.AllEntriesForDebug(), + Assert.Equal(Cell, owner.FullCellId); + Assert.Equal(new Vector3(18f, 12f, 7f), owner.PhysicsBody!.Position); + ShadowObjectRegistry shadows = lifetime.Physics.Engine.ShadowObjects; + uint ownerId = owner.Key!.Value.LocalEntityId; + ShadowEntry hiddenShadow = Assert.Single( + shadows.AllEntriesForDebug(), + entry => entry.EntityId == ownerId); + Assert.Equal(owner.PhysicsBody.Position, hiddenShadow.Position); + Assert.Equal(0, shadows.SuspendedRegistrationCount); + Assert.True(shadows.TryGetCollisionOwner( + ownerId, + out uint hiddenShadowState, + out _)); + Assert.Equal((uint)owner.FinalPhysicsState, hiddenShadowState); + Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection( + outcome.Projection)); + + Assert.True(lifetime.TryApplyState( + new SetState.Parsed( + owner.ServerGuid, + (uint)PhysicsStateFlags.ReportCollisions, + owner.Incarnation, + StateSequence: 3), + acknowledgeProjection: null, + out _, + out _)); + Assert.Equal(0, shadows.SuspendedRegistrationCount); + ShadowEntry restored = Assert.Single( + shadows.AllEntriesForDebug(), + entry => entry.EntityId == ownerId); + Assert.Equal(owner.PhysicsBody.Position, restored.Position); + Assert.Equal((uint)owner.FinalPhysicsState, restored.State); + } + + [Fact] + public void PreparedCollisionBatchStopsAfterCallbackHidesOwner() + { + using var lifetime = Lifetime(); + RuntimeEntityRecord owner = Entity( + lifetime, + 0x7000207Eu, + 1, + PhysicsStateFlags.ReportCollisions); + RuntimeEntityRecord first = Entity( + lifetime, 0x7000207Fu, 1, PhysicsStateFlags.None); + RuntimeEntityRecord second = Entity( + lifetime, 0x70002080u, 1, PhysicsStateFlags.None); + RegisterDynamicShadow(lifetime, owner); + RegisterDynamicShadow(lifetime, first); + RegisterDynamicShadow(lifetime, second); + bool hidden = false; + var observer = new CollisionObserver(report => + { + if (hidden + || report.Kind is not RuntimeCollisionReportKind.ObjectCollision + || report.Recipient != owner.Key) + { + return; + } + hidden = lifetime.TryApplyState( + new SetState.Parsed( + owner.ServerGuid, + (uint)(PhysicsStateFlags.ReportCollisions + | PhysicsStateFlags.Hidden), + owner.Incarnation, + StateSequence: 2), + acknowledgeProjection: null, + out _, + out _); + }); + using IDisposable subscription = lifetime.Physics.CollisionReports + .Subscribe(observer); + Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch( + owner, + owner.PhysicsBody!, + physicsTime: 12d, + previousContact: false, + previousOnWalkable: false, + finalOnWalkable: false, + collidedWithEnvironment: false, + [ + first.Key!.Value.LocalEntityId, + second.Key!.Value.LocalEntityId, + ], + out var prepared)); + Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch( + prepared!, out var receipt)); + + SetPositionCollisionBatchDispatchResult result = lifetime.Physics + .CollisionReports.DispatchSetPositionBatchResult(receipt); + + Assert.True(hidden); + Assert.Equal(SetPositionCollisionBatchDispatchStatus.Completed, + result.Status); + RuntimeCollisionReport report = Assert.Single( + observer.Reports, + candidate => candidate.Kind + is RuntimeCollisionReportKind.ObjectCollision); + Assert.Equal(first.Key, report.Other); + RuntimeCollisionReportingOwnershipSnapshot ownership = lifetime.Physics + .CollisionReports.CaptureOwnership(); + Assert.Equal(0, ownership.OwnerCount); + Assert.Equal(0, ownership.TrackedObjectCount); + Assert.Equal(0, ownership.ReversePeerCount); + Assert.Equal(0, ownership.PendingSetPositionDispatchCount); + ShadowObjectRegistry shadows = lifetime.Physics.Engine.ShadowObjects; + Assert.Equal(0, shadows.SuspendedRegistrationCount); + Assert.Contains( + shadows.AllEntriesForDebug(), entry => entry.EntityId == owner.Key!.Value.LocalEntityId); - Assert.Equal(initial, shadow.Position); + Assert.True(shadows.TryGetCollisionOwner( + owner.Key!.Value.LocalEntityId, + out uint shadowState, + out _)); + Assert.Equal((uint)owner.FinalPhysicsState, shadowState); + } + + [Fact] + public void NoDrawOwnerStillPublishesItsCollisionReport() + { + using var lifetime = Lifetime(); + RuntimeEntityRecord owner = Entity( + lifetime, + 0x7000207Au, + 1, + PhysicsStateFlags.ReportCollisions | PhysicsStateFlags.NoDraw); + RuntimeEntityRecord target = Entity( + lifetime, 0x7000207Bu, 1, PhysicsStateFlags.None); + RegisterDynamicShadow(lifetime, owner); + RegisterDynamicShadow(lifetime, target); + uint targetId = target.Key!.Value.LocalEntityId; + lifetime.Physics.Engine.TransitionCellCollisionTestHook = + (transition, phase, _, _) => + { + if (phase is TransitionCellCollisionPhase.Environment) + { + transition.CollisionInfo.CollideObjectGuids.Add(targetId); + transition.CollisionInfo.SetCollisionNormal(Vector3.UnitX); + } + return TransitionState.OK; + }; + var observer = new CollisionObserver(); + using IDisposable subscription = lifetime.Physics.CollisionReports + .Subscribe(observer); + + RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply( + owner, + owner.PositionAuthorityVersion, + PlacementCommand(owner, new Vector3(19f, 12f, 7f))); + + Assert.Equal( + RuntimeSetPositionStatus.CommittedHostAcknowledgementPending, + outcome.Status); + RuntimeCollisionReport report = Assert.Single( + observer.Reports, + report => report.Kind is RuntimeCollisionReportKind.ObjectCollision); + Assert.Equal(owner.Key, report.Recipient); + Assert.Equal(target.Key, report.Other); + Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection( + outcome.Projection)); + } + + [Fact] + public void NoDrawOwnerStillAllowsReciprocalPeerCollisionReport() + { + using var lifetime = Lifetime(); + RuntimeEntityRecord owner = Entity( + lifetime, 0x7000207Cu, 1, PhysicsStateFlags.NoDraw); + RuntimeEntityRecord target = Entity( + lifetime, + 0x7000207Du, + 1, + PhysicsStateFlags.ReportCollisions); + RegisterDynamicShadow(lifetime, owner); + RegisterDynamicShadow(lifetime, target); + uint targetId = target.Key!.Value.LocalEntityId; + lifetime.Physics.Engine.TransitionCellCollisionTestHook = + (transition, phase, _, _) => + { + if (phase is TransitionCellCollisionPhase.Environment) + { + transition.CollisionInfo.CollideObjectGuids.Add(targetId); + transition.CollisionInfo.SetCollisionNormal(Vector3.UnitX); + } + return TransitionState.OK; + }; + var observer = new CollisionObserver(); + using IDisposable subscription = lifetime.Physics.CollisionReports + .Subscribe(observer); + + RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply( + owner, + owner.PositionAuthorityVersion, + PlacementCommand(owner, new Vector3(20f, 12f, 7f))); + + Assert.Equal( + RuntimeSetPositionStatus.CommittedHostAcknowledgementPending, + outcome.Status); + RuntimeCollisionReport report = Assert.Single( + observer.Reports, + report => report.Kind is RuntimeCollisionReportKind.ObjectCollision); + Assert.Equal(target.Key, report.Recipient); + Assert.Equal(owner.Key, report.Other); + Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection( + outcome.Projection)); } [Fact] diff --git a/tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs b/tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs index aa7896e1..c3c34ab2 100644 --- a/tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs +++ b/tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs @@ -113,6 +113,42 @@ public sealed class RuntimeSetPositionStateTests Assert.Equal(1, ownership.PreparedMoverCount); } + [Theory] + [InlineData(PhysicsStateFlags.Hidden)] + [InlineData(PhysicsStateFlags.Hidden | PhysicsStateFlags.NoDraw)] + public void HiddenObjectCommitsResidenceWhileSuppressingCollisionReports( + PhysicsStateFlags suppressedState) + { + PhysicsEngine engine = FlatEngine(SourceLandblock, 0f); + using var lifetime = new RuntimeEntityObjectLifetime(engine); + RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001022u, 1); + PhysicsStateFlags state = PhysicsStateFlags.Gravity | suppressedState; + PhysicsBody body = AttachBody(lifetime, record, SourceCell, state); + var collisionObserver = new CollisionReportObserver(); + using IDisposable collisionSubscription = lifetime.Physics + .CollisionReports.Subscribe(collisionObserver); + + RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply( + record, + record.PositionAuthorityVersion, + Command(Request(SourceCell, new Vector3(12f, 18f, 7f)))); + + Assert.Equal( + RuntimeSetPositionStatus.CommittedHostAcknowledgementPending, + outcome.Status); + Assert.True(body.InWorld); + Assert.Equal(SourceCell, record.FullCellId); + Assert.Equal(new Vector3(12f, 18f, 7f), body.Position); + Assert.True(lifetime.Physics.IsSpatialRoot(record)); + Assert.Empty(collisionObserver.Reports); + RuntimeCollisionReportingOwnershipSnapshot reporting = lifetime + .Physics.CollisionReports.CaptureOwnership(); + Assert.Equal(0, reporting.OwnerCount); + Assert.Equal(0, reporting.PendingSetPositionDispatchCount); + Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection( + outcome.Projection)); + } + [Fact] public void PublicPlacementChannelObservesRetriesAndAcknowledgesExactToken() { @@ -2582,6 +2618,15 @@ public sealed class RuntimeSetPositionStateTests } } + private sealed class CollisionReportObserver + : IRuntimeCollisionReportObserver + { + internal List Reports { get; } = []; + + public void OnCollisionReport(in RuntimeCollisionReport report) => + Reports.Add(report); + } + private sealed class EntityObserver : IRuntimeEntityObjectObserver { internal List Deltas { get; } = [];