Follow-up to C5b (735f0a72). The retail-conformance review passed, so no production behaviour line moved: the flag truth table and the refreshPosition:false withhold are untouched. This is blast radius, test discrimination and documentation fidelity - plus two findings I could not confirm and am rebutting rather than complying with. D3 - THE PUBLISH-CONSERVATION TEST DID NOT DISCRIMINATE. The reviewer was right and it was the worst finding here: proof obligation 3's test passed identically with C5b reverted. Its only delta assertion FILTERED (Assert.Single(deltas, Rebucketed && parentGuid)), so the pre-C5b stream [Rebucketed] and the post-C5b stream [Updated, Rebucketed] both satisfied it, and childSpatialBefore+1 held in both worlds because whichever site did not move the cell propagated idempotently. It now asserts the complete ordered parent stream plus each element's CellId and Position.ObjCellId. Sabotage: restoring refreshPosition:acceptedPosition turns it red (it was green before), together with the withhold test and the new L5 test. That cardinality change was itself unfiled and is now AP-147: a cell-changing accepted Position publishes TWO entity deltas where it published one, and the intermediate Updated pairs the OLD CellId with the NEW wire Position - a torn pair that did not exist pre-C5b, since both halves used to move inside one publish. No production consumer reads a delta's paired fields, but a recorder/plugin/bot event log would capture it. The row states why suppressing the Updated is not available at that layer (the merge cannot know whether its caller reaches W2). D4 - THE PROJECTILE DOC COMMENT WAS FALSE AND ITS RETAIL ARGUMENT WAS INVERTED. SyncPresentationFromResolvedBody claimed record.FullCellId is "the WIRE cell ... stamped by the merge's RefreshDerivedState/SetFullCell, before classification ever runs" and argued from retail's store_position @0x00515CE2 that the destination cell is the right one. C5b falsified the premise; the missile arm also returns before W2, so nothing stamps the wire cell for a projectile at all. Rewritten. The honest conclusion, which the old text would have called wrong: on a stored outcome presentation now pairs the DESTINATION world position with the SOURCE cell. That is not a choice this method can make differently - StoreAcceptedDestinationPose writes only Position/Orientation, so record.FullCellId and body.CellPosition.ObjCellId now hold the same source cell and reading either yields the same value. The divergence is AP-138 item (1)'s store-writes-pose-but-not-cell residual, retiring via #309, not a field choice here. Projecting the wire cell instead would invent a residency the placement declined - the AP-1 shape C5b closed. L3/L4/L5 - PINNING GAPS, ALL THREE CONFIRMED AND CLOSED. L3: the matrix's oracle passed HasAnimations as a literal, so the merge's old.MotionTableId ?? old.Physics?.MotionTableId and RuntimeAcceptedPositionRouteRequests.Build's canonical-snapshot twin were textually identical and pinned by nothing. The oracle is now BUILT by the production constructor. L4: every fixture set both MotionTableId halves to the same value, so deleting either operand of the ?? was undetectable while the production comment said the mixed case is the real-world one. Six mixed rows added, including the explicit-zero row (a present-but-zero top half is not null, so ?? never reaches the physics half). L5: the retained Rebucketed ternary had zero coverage through TryApplyPosition - every restoreCancelledPark test called Forget directly. Now driven through the real merge, with the wire cell deliberately the SOURCE while the park's committed body cell is the DESTINATION, so the restored residency can only have come from the rollback. Sabotage (each red, each restored): merge ?? -> top half only, 1 red; -> physics half only, 2 red; Build's ?? -> physics half only, 2 red; ternary -> constant Updated, exactly the L5 test red. L1/L2 - THE MISSING TEST IS ADDED; THE DEFECT IS NOT THERE. The reviewer was right that C5b's "no fixture covers pickup at that layer" was inaccurate - LiveEntityNetworkOnPositionCollapseMatrixTests drives the real OnPosition at ~26 sites - and the end-to-end test is added: withdraw -> accepted Position -> IsSpatiallyProjected && FullCellId == wireCell, both guid classes. But ChildUnparentDisposition.Pending is NOT a live defect, because it is production-unreachable. The sole production _withdrawProjection binding (LivePresentationComposition.cs:599) is LiveEntityProjectionWithdrawalController.WithdrawExact, whose only Pending mint is inside its catch block and therefore always carries a non-null Failure - and AdvanceUnparentTransition rethrows at EquippedChildRenderController.cs:1307 BEFORE the return Pending at :1309. The named drop scenario does not reach it anyway (BeginDetachedRemoval has already emptied the capture list) and would be correct if it did: a previously-equipped child is LegacyImmediate, so the FullCellId != 0u gate at DatLiveEntityProjectionMaterializer.cs:767 is never consulted and re-projection uses the wire cell at LiveEntityRuntime.cs:824. Measured while building that test, and NOT what C5b assumed: W2 and W3 are REDUNDANT on the remote tail. Sabotaging W2 alone - adopting the committed cell instead of the wire cell, OR skipping the rebucket outright - leaves the whole file green, because W3's RemoteMotion.CellId write reads through to canonical FullCellId via CommitCanonicalCell, whose CellCommitted recovery re-installs the bucket. Only removing BOTH goes red, and then the new test is the only red in the file. So it is named for what it pins, and AD-60 is amended with the measurement: neither channel is individually load-bearing, so a future retirement of one is caught by nothing else. D2 - REBUTTED, WITH THE REAL GAP FILED INSTEAD. The reviewer's hypothesis was that TryApplyInitialCreateCompletionPresentation's staleness guard lost its ability to detect an intervening steady-state Position when C5b stopped the merge stamping the wire cell, and asked for a PositionAuthorityVersion term. I do not think that is right and did not add it. The receipt's facts are the canonical BODY's pose and cell at publish (PublishExecutorCompletion builds both from the record). Exactly two owners can move them: a Runtime SetPosition commit/withdrawal, every one of which calls AdvancePlacementCommit - the only caller family is RuntimeSetPositionState - and a rebucket, which moves FullCellId. Both are already covered by the two existing terms. An accepted steady-state Position is neither, and C5b did not make it one: the merge refreshes the snapshot and advances PositionAuthorityVersion but never wrote the body, and the App generic tail writes the RENDER entity. The wire-cell half stays covered because W2/W3 commit it in the same call; the paths that return before them leave the record at the last committed cell, which IS the receipt's own cell - correctly not a supersession. Adding the term would decline receipts whose facts are still true, on the entity's FIRST world-visible moment: the pose write and RebucketLiveEntityPresentationOnly would be skipped while TryPublishPlace still publishes, so a packet returning before the render write would leave the sidecar visible at its materialized pose in a wrong bucket. That is the handoff's own "removed the invariant failure while leaving the bug" shape. There IS one supersession neither term covers, and it predates C5b: RuntimeRemotePlacementDriveController.StoreAcceptedDestinationPose writes body.Position/Orientation on the far-snap Refused/Contention arm with no placement commit and no cell move. Filed as #323 with the FIFO-blocking argument for why a receipt can still be pending when it lands, an explicit "not established as reachable", and an explicit "do not fix it with PositionAuthorityVersion". The guard's comment now carries the whole argument instead of one sentence. S1 - DANGLING POINTER CLOSED. InboundPhysicsStateController.cs:610 still said the two-callers-one-rule debt was "tracked for the eventual cutover unification ... See docs/ISSUES.md", which pointed at nothing after C5b closed #275 without a successor. Filed #322, cited from both the comment and #275's closure, including why widening TryApplyPosition's signature to take a route would be the wrong unification. AP-138 amended: C5b staled its round-3 measurement that "both accepted-Position callers commit the accepted wire cell to record.FullCellId before submitting". Route 2 submits from TryExecuteAcceptedLocalPosition ahead of W2, so on a first submit PlacementTouchesPrefix's CurrentCellId arm now names the SOURCE landblock, not the destination. Confined to which prefix the quiescence pre-flight matches, which that row already established is not the correctness mechanism. GATES. Release build 0 errors. Complete suite 11,134 passed / 4 skipped / 0 failed, from the 11,125 / 4 baseline ated806997: net +9, all new tests, no test deleted or weakened, no new skip. Runtime.Tests 1195 -> 1202 (+6 mixed-motion-table rows, +1 park-rollback fact); App.Tests 4132 -> 4134 (+2 guid rows). None of #302/#308/#321 appeared. Not connected-gated - nothing here changes runtime behaviour. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3818 lines
166 KiB
C#
3818 lines
166 KiB
C#
using System.Collections.Immutable;
|
|
using System.Numerics;
|
|
using System.Reflection;
|
|
using AcDream.Content;
|
|
using AcDream.Content.Pak;
|
|
using AcDream.Core.Net;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Physics.Motion;
|
|
using AcDream.Core.World.Cells;
|
|
using AcDream.Runtime.Entities;
|
|
using AcDream.Runtime.Physics;
|
|
using DatReaderWriter.Enums;
|
|
using DatReaderWriter.Types;
|
|
|
|
namespace AcDream.Runtime.Tests.Physics;
|
|
|
|
public sealed class RuntimeSetPositionStateTests
|
|
{
|
|
private const uint SourceLandblock = 0xA9B40000u;
|
|
private const uint SourceCell = SourceLandblock | 0x0001u;
|
|
private const uint DestinationLandblock = 0xAAB40000u;
|
|
private const uint DestinationCell = DestinationLandblock | 0x0001u;
|
|
private const uint DestinationIndoorCell = DestinationLandblock | 0x0100u;
|
|
|
|
[Fact]
|
|
public void AcceptedTokenExistsBeforeHostPreparationAndRejectsInvalidPortalAuthority()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001001u, 1);
|
|
|
|
RuntimeEntityPlacementToken token = lifetime.Physics.SetPosition
|
|
.BeginAcceptedPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
|
|
Assert.True(token.IsValid);
|
|
Assert.Equal(record.Key, token.Entity);
|
|
Assert.Equal(1, lifetime.Physics.CaptureOwnership()
|
|
.AwaitingSetPositionPreparationCount);
|
|
|
|
var invalidPortal = new RuntimePortalPlacementAuthority(
|
|
Present: true,
|
|
RevealGeneration: 7,
|
|
TeleportSequence: 0,
|
|
new RuntimeWorldHostProjectionToken(7, DestinationCell));
|
|
Assert.False(lifetime.Physics.SetPosition.BeginAcceptedPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative,
|
|
invalidPortal).IsValid);
|
|
|
|
// 4B1 validates the immutable authority shape. The authoritative
|
|
// RuntimeWorldTransitState membership check is intentionally the
|
|
// tested 4B2 routing seam.
|
|
RuntimeEntityPlacementToken portalToken = lifetime.Physics.SetPosition
|
|
.BeginAcceptedPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.LocalAuthoritative,
|
|
invalidPortal);
|
|
Assert.False(portalToken.IsValid);
|
|
|
|
var matchingPortal = invalidPortal with
|
|
{
|
|
Projection = new RuntimeWorldHostProjectionToken(7, SourceCell),
|
|
};
|
|
Assert.True(lifetime.Physics.SetPosition.BeginAuthoredPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.LocalAuthoritative,
|
|
matchingPortal).IsValid);
|
|
}
|
|
|
|
[Fact]
|
|
public void ImmediateCommitIsCanonicalBeforeProjectionAndExactTokenRetriesUntilAck()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001002u, 1);
|
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
|
|
var observer = new PlacementObserver(delta =>
|
|
{
|
|
Assert.Equal(RuntimePlacementProjectionKind.Place,
|
|
delta.Placement.Kind);
|
|
Assert.Same(body, record.PhysicsBody);
|
|
Assert.Equal(SourceCell, record.FullCellId);
|
|
Assert.Equal(new Vector3(12f, 18f, 7f), body.Position);
|
|
Assert.True(body.InWorld);
|
|
Assert.True(lifetime.Physics.IsSpatialRoot(record));
|
|
});
|
|
using IDisposable subscription = lifetime.Events.SubscribePlacement(observer);
|
|
|
|
RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(Request(SourceCell, new Vector3(12f, 18f, 7f))));
|
|
|
|
Assert.Equal(
|
|
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
|
|
outcome.Status);
|
|
Assert.Single(observer.Deltas);
|
|
RuntimePlacementProjectionToken token = outcome.Projection;
|
|
lifetime.Physics.SetPosition.RetryPendingProjections();
|
|
Assert.Equal(2, observer.Deltas.Count);
|
|
Assert.Equal(token, observer.Deltas[0].Placement.Token);
|
|
Assert.Equal(token, observer.Deltas[1].Placement.Token);
|
|
Assert.True(observer.Deltas[1].Stamp.Sequence
|
|
> observer.Deltas[0].Stamp.Sequence);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(token));
|
|
RuntimeSetPositionOwnershipSnapshot ownership =
|
|
lifetime.Physics.SetPosition.CaptureOwnership();
|
|
Assert.Equal(0, ownership.ActiveOperationCount);
|
|
Assert.Equal(0, ownership.PendingProjectionAcknowledgementCount);
|
|
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()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
var generation = new RuntimeGenerationToken(7UL);
|
|
lifetime.BindEventContext(() => generation, static () => 11UL);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001012u, 1);
|
|
_ = AttachBody(lifetime, record, SourceCell);
|
|
var observer = new PlacementObserver();
|
|
using IDisposable subscription = lifetime.Placements.Subscribe(observer);
|
|
|
|
RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(Request(SourceCell, new Vector3(14f, 19f, 7f))));
|
|
|
|
Assert.Single(observer.Deltas);
|
|
Assert.True(lifetime.Placements.TryPeek(generation, out var pending));
|
|
Assert.Equal(outcome.Projection, pending.Token);
|
|
Assert.True(lifetime.Placements.RetryPending(generation));
|
|
Assert.Equal(2, observer.Deltas.Count);
|
|
Assert.Equal(
|
|
observer.Deltas[0].Placement,
|
|
observer.Deltas[1].Placement);
|
|
|
|
RuntimePlacementProjectionToken stale = outcome.Projection with
|
|
{
|
|
Revision = outcome.Projection.Revision + 1UL,
|
|
};
|
|
Assert.False(lifetime.Placements.Acknowledge(generation, stale));
|
|
Assert.False(lifetime.Placements.RetryPending(
|
|
new RuntimeGenerationToken(8UL)));
|
|
Assert.False(lifetime.Placements.TryPeek(
|
|
new RuntimeGenerationToken(8UL),
|
|
out _));
|
|
Assert.True(lifetime.Placements.Acknowledge(
|
|
generation,
|
|
outcome.Projection));
|
|
Assert.Equal(0, lifetime.Placements.PendingCount);
|
|
Assert.False(lifetime.Placements.TryPeek(generation, out _));
|
|
Assert.False(lifetime.Placements.Acknowledge(
|
|
generation,
|
|
outcome.Projection));
|
|
}
|
|
|
|
[Fact]
|
|
public void InWorldSetPositionPreservesObjectClockEpochPendingTimeAndActiveState()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001110u, 1);
|
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
|
|
body.LastUpdateTime = 3d;
|
|
_ = record.ObjectClock.Advance(0.01d);
|
|
ulong epoch = record.ObjectClockEpoch;
|
|
|
|
RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(Request(SourceCell, new Vector3(11f, 18f, 7f))));
|
|
|
|
Assert.Equal(
|
|
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
|
|
outcome.Status);
|
|
Assert.Equal(epoch, record.ObjectClockEpoch);
|
|
Assert.Equal(3d, body.LastUpdateTime);
|
|
Assert.Equal(0.01d, record.ObjectClock.PendingSeconds, precision: 12);
|
|
Assert.True(record.ObjectClock.IsActive);
|
|
Assert.True(body.TransientState.HasFlag(TransientStateFlags.Active));
|
|
}
|
|
|
|
[Fact]
|
|
public void InWorldSetPositionDoesNotWakeAnInactiveBody()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001111u, 1);
|
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
|
|
body.LastUpdateTime = 4d;
|
|
record.ObjectClock.Deactivate();
|
|
body.TransientState &= ~TransientStateFlags.Active;
|
|
ulong epoch = record.ObjectClockEpoch;
|
|
|
|
RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(Request(SourceCell, new Vector3(11f, 19f, 7f))));
|
|
|
|
Assert.Equal(
|
|
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
|
|
outcome.Status);
|
|
Assert.Equal(epoch, record.ObjectClockEpoch);
|
|
Assert.Equal(4d, body.LastUpdateTime);
|
|
Assert.False(record.ObjectClock.IsActive);
|
|
Assert.False(body.TransientState.HasFlag(TransientStateFlags.Active));
|
|
}
|
|
|
|
[Fact]
|
|
public void WarmedImmediateCommitAllocationIsMeasuredBeforeProductionCutover()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x7000111Cu, 1);
|
|
_ = AttachBody(lifetime, record, SourceCell);
|
|
RuntimeSetPositionCommand command = Command(Request(
|
|
SourceCell,
|
|
new Vector3(11f, 18f, 7f)));
|
|
for (int warm = 0; warm < 64; warm++)
|
|
{
|
|
RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
command);
|
|
if (!lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
outcome.Projection))
|
|
{
|
|
throw new InvalidOperationException("Warmup acknowledgement failed.");
|
|
}
|
|
}
|
|
|
|
const int iterations = 1_000;
|
|
long before = GC.GetAllocatedBytesForCurrentThread();
|
|
for (int iteration = 0; iteration < iterations; iteration++)
|
|
{
|
|
RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
command);
|
|
if (!lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
outcome.Projection))
|
|
{
|
|
throw new InvalidOperationException("Measured acknowledgement failed.");
|
|
}
|
|
}
|
|
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
|
|
|
// C2: root-caused and fixed the dormant 4B1 owner's per-operation
|
|
// allocations instead of raising this cap. Measured exactly 944 B/op
|
|
// (stable across 5+ repeat runs), down from the pre-fix 2,032 B/op -
|
|
// pooling `Operation` instances (BeginAcceptedPlacementCore no
|
|
// longer allocates a fresh instance every accepted placement),
|
|
// caching the `PhysicsEngine.SetPosition` collision-report delegate
|
|
// instead of a per-call closure, and replacing the LINQ
|
|
// `_pendingProjection.First()` pattern (which boxed
|
|
// SortedDictionary's struct Enumerator through IEnumerable<T> every
|
|
// acknowledgement) with a non-boxing foreach. See
|
|
// docs/research/2026-07-31-canonical-set-position.md for the C2
|
|
// finding this closes and the accepted residual floor: ~520 B/op
|
|
// lives inside AcDream.Core's PhysicsEngine.SetPosition (transition
|
|
// init/inner solve/query-footprint materialization - shared physics
|
|
// infrastructure, out of this Runtime-only slice's scope) and
|
|
// ~208 B/op is SortedDictionary's inherent per-Add tree-node
|
|
// allocation for `_pendingProjection` (replacing that ordered
|
|
// structure to chase the last ~230 B/op was judged too invasive/
|
|
// risky for the remaining headroom under this cap). 1,536 keeps
|
|
// roughly 60% headroom over the measured value for JIT/environment
|
|
// variance without re-opening the door to unbounded per-op growth.
|
|
Assert.InRange(allocated / iterations, 1L, 1_536L);
|
|
}
|
|
|
|
[Fact]
|
|
public void MissingCrossLandblockCellParksExactBodyThenMatchingCellGenerationWakesIt()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
var gameClock = new GameRuntimeClock();
|
|
_ = gameClock.Advance(35d);
|
|
var time = new ManualTimeProvider(
|
|
DateTimeOffset.UnixEpoch.AddSeconds(100d));
|
|
using var lifetime = new RuntimeEntityObjectLifetime(
|
|
engine,
|
|
timeProvider: time,
|
|
gameClock: gameClock);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001003u, 1);
|
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
|
|
var remote = new RemoteMotion(body)
|
|
{
|
|
HasServerVelocity = true,
|
|
ServerVelocity = Vector3.UnitX,
|
|
};
|
|
lifetime.Physics.SetRemoteMotion(record, remote);
|
|
body.TransientState = TransientStateFlags.Active
|
|
| TransientStateFlags.Contact
|
|
| TransientStateFlags.OnWalkable
|
|
| TransientStateFlags.WaterContact
|
|
| TransientStateFlags.Sliding;
|
|
body.ContactPlaneValid = true;
|
|
body.ContactPlaneIsWater = true;
|
|
body.SlidingNormal = Vector3.UnitX;
|
|
body.set_velocity(new Vector3(3f, 4f, 5f));
|
|
engine.ShadowObjects.Register(
|
|
record.Key!.Value.LocalEntityId,
|
|
0x01000001u,
|
|
body.Position,
|
|
body.Orientation,
|
|
0.48f,
|
|
0f,
|
|
0f,
|
|
SourceLandblock,
|
|
seedCellId: SourceCell,
|
|
isStatic: false);
|
|
var observer = new PlacementObserver();
|
|
using IDisposable subscription = lifetime.Events.SubscribePlacement(observer);
|
|
|
|
RuntimeSetPositionOutcome deferred = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(CrossLandblockRequest()));
|
|
|
|
Assert.Equal(RuntimeSetPositionStatus.DeferredCell, deferred.Status);
|
|
Assert.Equal(DestinationCell, deferred.ExactCellId);
|
|
Assert.Same(body, record.PhysicsBody);
|
|
Assert.Equal(DestinationCell, body.CellPosition.ObjCellId);
|
|
Assert.Equal(0u, record.FullCellId);
|
|
Assert.False(body.InWorld);
|
|
Assert.Equal(1d, body.LastUpdateTime);
|
|
Assert.False(body.TransientState.HasFlag(TransientStateFlags.Active));
|
|
Assert.True(body.InContact);
|
|
Assert.True(body.OnWalkable);
|
|
Assert.True(body.ContactPlaneIsWater);
|
|
Assert.Equal(Vector3.UnitX, body.SlidingNormal);
|
|
Assert.Equal(new Vector3(3f, 4f, 5f), body.Velocity);
|
|
Assert.False(lifetime.Physics.IsSpatialRoot(record));
|
|
Assert.Equal(1, engine.ShadowObjects.SuspendedRegistrationCount);
|
|
Assert.Equal(RuntimePlacementProjectionKind.Withdraw,
|
|
Assert.Single(observer.Deltas).Placement.Kind);
|
|
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
deferred.Projection));
|
|
ulong suspendedEpoch = record.ObjectClockEpoch;
|
|
Assert.False(record.ObjectClock.IsActive);
|
|
lifetime.Physics.SetPosition.CommitCollisionGeneration(
|
|
DestinationLandblock,
|
|
generation: 2,
|
|
ready: true);
|
|
Assert.Single(observer.Deltas);
|
|
|
|
AddFlatLandblock(engine, DestinationLandblock, 192f);
|
|
_ = gameClock.Advance(5d);
|
|
lifetime.Physics.SetPosition.CommitCollisionGeneration(
|
|
DestinationLandblock,
|
|
generation: 1,
|
|
ready: true);
|
|
|
|
Assert.Equal(2, observer.Deltas.Count);
|
|
RuntimePlacementProjectionSnapshot placed = observer.Deltas[1].Placement;
|
|
Assert.Equal(RuntimePlacementProjectionKind.Place, placed.Kind);
|
|
Assert.Equal(DestinationCell, placed.Token.ExactCellId);
|
|
Assert.Same(body, record.PhysicsBody);
|
|
Assert.Equal(DestinationCell, record.FullCellId);
|
|
Assert.True(body.InWorld);
|
|
Assert.True(double.IsFinite(body.LastUpdateTime));
|
|
Assert.Equal(40d, body.LastUpdateTime);
|
|
Assert.Equal(suspendedEpoch + 1UL, record.ObjectClockEpoch);
|
|
Assert.Equal(0d, record.ObjectClock.PendingSeconds);
|
|
Assert.True(record.ObjectClock.IsActive);
|
|
Assert.True(body.TransientState.HasFlag(TransientStateFlags.Active));
|
|
Assert.True(lifetime.Physics.IsSpatialRoot(record));
|
|
Assert.Equal(100d, remote.LastServerPosTime);
|
|
var remoteUpdater = new RuntimeRemotePhysicsUpdater(
|
|
lifetime.Physics);
|
|
Assert.True(remoteUpdater.Tick(
|
|
record,
|
|
remote,
|
|
objectScale: 1f,
|
|
sequencer: null,
|
|
dt: 0.01f,
|
|
objectClockEpoch: record.ObjectClockEpoch,
|
|
new MotionDeltaFrame
|
|
{
|
|
Orientation = Quaternion.Identity,
|
|
},
|
|
radius: 0.48f,
|
|
height: 1.835f,
|
|
liveCenterX: 1,
|
|
liveCenterY: 1));
|
|
Assert.True(remote.HasServerVelocity);
|
|
Assert.Equal(Vector3.UnitX, remote.ServerVelocity);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
placed.Token));
|
|
Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership()
|
|
.ActiveOperationCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void FailedDeferredWakeRetainsLostOwnerDeadlineAndRetries()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001121u, 1);
|
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
|
|
var observer = new PlacementObserver();
|
|
using IDisposable subscription = lifetime.Events.SubscribePlacement(observer);
|
|
RuntimeSetPositionOutcome deferred = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(CrossLandblockRequest()));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
deferred.Projection));
|
|
|
|
AddFlatLandblock(engine, DestinationLandblock, 192f);
|
|
engine.TransitionCellCollisionTestHook =
|
|
static (_, _, _, _) => TransitionState.Collided;
|
|
lifetime.Physics.SetPosition.CommitCollisionGeneration(
|
|
DestinationLandblock,
|
|
generation: 1,
|
|
ready: true);
|
|
|
|
RuntimeSetPositionOwnershipSnapshot failed = lifetime.Physics
|
|
.SetPosition.CaptureOwnership();
|
|
Assert.Equal(1, failed.ActiveOperationCount);
|
|
Assert.Equal(1, failed.DeferredCellCount);
|
|
Assert.Equal(1, failed.LostDeadlineCount);
|
|
Assert.Equal(1, failed.DeferredBucketCount);
|
|
Assert.True(failed.IndexesConsistent);
|
|
Assert.Equal(0u, record.FullCellId);
|
|
Assert.False(body.InWorld);
|
|
Assert.Single(observer.Deltas);
|
|
|
|
engine.TransitionCellCollisionTestHook = null;
|
|
lifetime.Physics.SetPosition.CommitCollisionGeneration(
|
|
DestinationLandblock,
|
|
generation: 1,
|
|
ready: true);
|
|
RuntimePlacementProjectionSnapshot placed = observer.Deltas[^1].Placement;
|
|
Assert.Equal(RuntimePlacementProjectionKind.Place, placed.Kind);
|
|
Assert.Equal(record.Key, placed.Token.Entity);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
placed.Token));
|
|
Assert.Equal(DestinationCell, record.FullCellId);
|
|
Assert.True(body.InWorld);
|
|
}
|
|
|
|
[Fact]
|
|
public void StandaloneDeferredWakeRetainsAcceptedSimulationTimeFallback()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001126u, 1);
|
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
|
|
RuntimeSetPositionOutcome deferred = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(CrossLandblockRequest()));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
deferred.Projection));
|
|
AddFlatLandblock(engine, DestinationLandblock, 192f);
|
|
|
|
lifetime.Physics.SetPosition.CommitCollisionGeneration(
|
|
DestinationLandblock,
|
|
generation: 1,
|
|
ready: true);
|
|
|
|
Assert.Equal(10d, body.LastUpdateTime);
|
|
Assert.True(lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot placed));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
placed.Token));
|
|
}
|
|
|
|
[Fact]
|
|
public void LegacyDeferredWakeCannotBypassNewAuthoredSealAfterAuthorityStales()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x7000112Bu, 1);
|
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
|
|
var observer = new PlacementObserver();
|
|
using IDisposable subscription = lifetime.Events.SubscribePlacement(
|
|
observer);
|
|
RuntimeSetPositionOutcome deferred = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(CrossLandblockRequest()));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
deferred.Projection));
|
|
|
|
// The direct legacy command was valid when parked, but any accepted
|
|
// authority replacement requires a new authored mover preparation.
|
|
lifetime.Entities.AdvanceObjDescAuthority(record);
|
|
AddFlatLandblock(engine, DestinationLandblock, 192f);
|
|
lifetime.Physics.SetPosition.CommitCollisionGeneration(
|
|
DestinationLandblock,
|
|
generation: 1,
|
|
ready: true);
|
|
|
|
Assert.False(body.InWorld);
|
|
Assert.True(lifetime.Physics.SetPosition.TryGetAwaitingPreparationToken(
|
|
record,
|
|
out RuntimeEntityPlacementToken token));
|
|
Assert.Equal(RuntimeEntityPlacementPreparationKind.LegacyDirect,
|
|
token.PreparationKind);
|
|
RuntimeSetPositionOutcome bypass = lifetime.Physics.SetPosition
|
|
.SubmitPreparedPlacement(token, Command(CrossLandblockRequest()));
|
|
Assert.Equal(RuntimeSetPositionStatus.Rejected, bypass.Status);
|
|
Assert.False(body.InWorld);
|
|
|
|
RuntimeSetPositionCommand prepared = PrepareAuthoredCommand(
|
|
lifetime,
|
|
token,
|
|
[new FlatCollisionSphere(Vector3.Zero, 0.4f)]);
|
|
RuntimeSetPositionOutcome resumed = lifetime.Physics.SetPosition
|
|
.SubmitPreparedPlacement(token, prepared);
|
|
Assert.Equal(PhysicsResidenceDisposition.Committed, resumed.Residence);
|
|
RuntimePlacementProjectionSnapshot placed = observer.Deltas[^1].Placement;
|
|
Assert.Equal(RuntimePlacementProjectionKind.Place, placed.Kind);
|
|
Assert.True(body.InWorld);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
placed.Token));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(DeferredPhysicsStateMutation.ChildNoDraw)]
|
|
[InlineData(DeferredPhysicsStateMutation.StopMissile)]
|
|
[InlineData(DeferredPhysicsStateMutation.DirectFinalState)]
|
|
public void DeferredWakeRequiresRepreparationAfterFinalPhysicsStateMutation(
|
|
DeferredPhysicsStateMutation mutation)
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x7000112Cu, 1);
|
|
PhysicsStateFlags initial = PhysicsStateFlags.Gravity;
|
|
if (mutation is DeferredPhysicsStateMutation.StopMissile)
|
|
{
|
|
initial |= PhysicsStateFlags.Missile
|
|
| PhysicsStateFlags.AlignPath
|
|
| PhysicsStateFlags.PathClipped;
|
|
}
|
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell, initial);
|
|
RuntimeSetPositionOutcome deferred = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(CrossLandblockRequest()));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
deferred.Projection));
|
|
ulong priorMutation = record.PhysicsStateMutationVersion;
|
|
|
|
switch (mutation)
|
|
{
|
|
case DeferredPhysicsStateMutation.ChildNoDraw:
|
|
lifetime.Entities.SetChildNoDraw(record, noDraw: true);
|
|
break;
|
|
case DeferredPhysicsStateMutation.StopMissile:
|
|
Assert.True(lifetime.Entities.StopMissileAfterCollision(
|
|
record,
|
|
requireCurrentMissile: true));
|
|
break;
|
|
case DeferredPhysicsStateMutation.DirectFinalState:
|
|
lifetime.Entities.SetFinalPhysicsState(
|
|
record,
|
|
record.FinalPhysicsState | PhysicsStateFlags.Frozen);
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException(nameof(mutation));
|
|
}
|
|
Assert.Equal(priorMutation + 1UL,
|
|
record.PhysicsStateMutationVersion);
|
|
if (mutation is DeferredPhysicsStateMutation.DirectFinalState)
|
|
Assert.NotEqual(record.FinalPhysicsState, body.State);
|
|
else
|
|
Assert.Equal(record.FinalPhysicsState, body.State);
|
|
|
|
AddFlatLandblock(engine, DestinationLandblock, 192f);
|
|
lifetime.Physics.SetPosition.CommitCollisionGeneration(
|
|
DestinationLandblock,
|
|
generation: 1,
|
|
ready: true);
|
|
|
|
Assert.False(body.InWorld);
|
|
Assert.True(lifetime.Physics.SetPosition.TryGetAwaitingPreparationToken(
|
|
record,
|
|
out _));
|
|
Assert.Equal(1, lifetime.Physics.CaptureOwnership()
|
|
.AwaitingSetPositionPreparationCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void DirectFinalPhysicsStateMutationAdvancesOnlyOnChangeAndLeavesBodyToItsOwner()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x7000112Du, 1);
|
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
|
|
PhysicsStateFlags original = record.FinalPhysicsState;
|
|
ulong version = record.PhysicsStateMutationVersion;
|
|
|
|
lifetime.Entities.SetFinalPhysicsState(record, original);
|
|
Assert.Equal(version, record.PhysicsStateMutationVersion);
|
|
Assert.Equal(original, body.State);
|
|
|
|
PhysicsStateFlags changed = original | PhysicsStateFlags.Frozen;
|
|
lifetime.Entities.SetFinalPhysicsState(record, changed);
|
|
Assert.Equal(version + 1UL, record.PhysicsStateMutationVersion);
|
|
Assert.Equal(changed, record.FinalPhysicsState);
|
|
Assert.Equal(original, body.State);
|
|
|
|
lifetime.Entities.SetFinalPhysicsState(record, changed);
|
|
Assert.Equal(version + 1UL, record.PhysicsStateMutationVersion);
|
|
Assert.Equal(original, body.State);
|
|
}
|
|
|
|
[Fact]
|
|
public void RejectedPreparationKeepsValidatedMoverAndExactTokenRetryable()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001122u, 1);
|
|
_ = AttachBody(lifetime, record, SourceCell);
|
|
ImmutableArray<FlatCollisionSphere> validated =
|
|
[
|
|
new FlatCollisionSphere(new Vector3(0.25f, 0f, 0f), 0.3f),
|
|
new FlatCollisionSphere(new Vector3(-0.2f, 0f, 0.4f), 0.2f),
|
|
];
|
|
RuntimeSetPositionOutcome seeded = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(Request(
|
|
SourceCell,
|
|
new Vector3(12f, 18f, 7f)) with
|
|
{
|
|
Spheres = validated,
|
|
}));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
seeded.Projection));
|
|
Assert.True(lifetime.Physics.SetPosition.TryGetPreparedMoverSphereCount(
|
|
record,
|
|
out int sphereCount));
|
|
Assert.Equal(2, sphereCount);
|
|
|
|
RuntimeEntityPlacementToken token = lifetime.Physics.SetPosition
|
|
.BeginAcceptedPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
RuntimeSetPositionCommand replacement = Command(Request(
|
|
SourceCell,
|
|
new Vector3(13f, 18f, 7f)) with
|
|
{
|
|
Spheres =
|
|
[
|
|
new FlatCollisionSphere(Vector3.Zero, 0.4f),
|
|
],
|
|
});
|
|
engine.TransitionCellCollisionTestHook =
|
|
static (_, _, _, _) => TransitionState.Collided;
|
|
RuntimeSetPositionOutcome rejected = lifetime.Physics.SetPosition
|
|
.SubmitPreparedPlacement(token, replacement);
|
|
Assert.Equal(RuntimeSetPositionStatus.Rejected, rejected.Status);
|
|
Assert.True(lifetime.Physics.SetPosition.TryGetPreparedMoverSphereCount(
|
|
record,
|
|
out sphereCount));
|
|
Assert.Equal(2, sphereCount);
|
|
|
|
RuntimeSetPositionOutcome malformed = lifetime.Physics.SetPosition
|
|
.SubmitPreparedPlacement(
|
|
token,
|
|
replacement with { GameTime = double.NaN });
|
|
Assert.Equal(PhysicsSetPositionError.InvalidArguments, malformed.Error);
|
|
Assert.True(lifetime.Physics.SetPosition.TryGetAwaitingPreparationToken(
|
|
record,
|
|
out RuntimeEntityPlacementToken retryToken));
|
|
Assert.Equal(token, retryToken);
|
|
|
|
engine.TransitionCellCollisionTestHook = null;
|
|
RuntimeSetPositionOutcome retried = lifetime.Physics.SetPosition
|
|
.SubmitPreparedPlacement(retryToken, replacement);
|
|
Assert.Equal(
|
|
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
|
|
retried.Status);
|
|
Assert.True(lifetime.Physics.SetPosition.TryGetPreparedMoverSphereCount(
|
|
record,
|
|
out sphereCount));
|
|
Assert.Equal(1, sphereCount);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
retried.Projection));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0u)]
|
|
[InlineData(SourceLandblock | 0x0041u)]
|
|
public void InvalidRetailCellFrameRejectsBeforeCoreAndPreparedCache(
|
|
uint invalidCellId)
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001128u, 1);
|
|
_ = AttachBody(lifetime, record, SourceCell);
|
|
RuntimeEntityPlacementToken token = lifetime.Physics.SetPosition
|
|
.BeginAcceptedPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
int collisionCalls = 0;
|
|
engine.TransitionCellCollisionTestHook = (_, _, _, _) =>
|
|
{
|
|
collisionCalls++;
|
|
return TransitionState.OK;
|
|
};
|
|
|
|
RuntimeSetPositionOutcome rejected = lifetime.Physics.SetPosition
|
|
.SubmitPreparedPlacement(
|
|
token,
|
|
Command(Request(
|
|
invalidCellId,
|
|
new Vector3(12f, 18f, 7f))));
|
|
|
|
Assert.Equal(RuntimeSetPositionStatus.Rejected, rejected.Status);
|
|
Assert.Equal(PhysicsSetPositionError.InvalidArguments, rejected.Error);
|
|
Assert.Equal(0, collisionCalls);
|
|
Assert.False(lifetime.Physics.SetPosition
|
|
.TryGetPreparedMoverSphereCount(record, out _));
|
|
Assert.True(lifetime.Physics.SetPosition.TryGetAwaitingPreparationToken(
|
|
record,
|
|
out RuntimeEntityPlacementToken retry));
|
|
Assert.Equal(token, retry);
|
|
}
|
|
|
|
[Fact]
|
|
public void InvalidRetailQuaternionRejectsBeforeCoreAndPreparedCache()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001129u, 1);
|
|
_ = AttachBody(lifetime, record, SourceCell);
|
|
RuntimeEntityPlacementToken token = lifetime.Physics.SetPosition
|
|
.BeginAcceptedPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
int collisionCalls = 0;
|
|
engine.TransitionCellCollisionTestHook = (_, _, _, _) =>
|
|
{
|
|
collisionCalls++;
|
|
return TransitionState.OK;
|
|
};
|
|
Quaternion[] invalid =
|
|
[
|
|
Quaternion.Zero,
|
|
new Quaternion(0f, 0f, 0f, 0.9f),
|
|
];
|
|
|
|
foreach (Quaternion orientation in invalid)
|
|
{
|
|
RuntimeSetPositionOutcome rejected = lifetime.Physics.SetPosition
|
|
.SubmitPreparedPlacement(
|
|
token,
|
|
Command(Request(
|
|
SourceCell,
|
|
new Vector3(12f, 18f, 7f)) with
|
|
{
|
|
Orientation = orientation,
|
|
}));
|
|
Assert.Equal(RuntimeSetPositionStatus.Rejected, rejected.Status);
|
|
Assert.Equal(
|
|
PhysicsSetPositionError.InvalidArguments,
|
|
rejected.Error);
|
|
}
|
|
|
|
Assert.Equal(0, collisionCalls);
|
|
Assert.False(lifetime.Physics.SetPosition
|
|
.TryGetPreparedMoverSphereCount(record, out _));
|
|
Assert.True(lifetime.Physics.SetPosition.TryGetAwaitingPreparationToken(
|
|
record,
|
|
out RuntimeEntityPlacementToken retry));
|
|
Assert.Equal(token, retry);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExtremeScatterAttemptsRejectWithoutEnteringSynchronousLoop()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x7000112Au, 1);
|
|
_ = AttachBody(lifetime, record, SourceCell);
|
|
RuntimeEntityPlacementToken token = lifetime.Physics.SetPosition
|
|
.BeginAcceptedPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
int randomDraws = 0;
|
|
engine.SetPositionRandomUnit = () =>
|
|
{
|
|
randomDraws++;
|
|
return 0.5d;
|
|
};
|
|
|
|
RuntimeSetPositionOutcome rejected = lifetime.Physics.SetPosition
|
|
.SubmitPreparedPlacement(
|
|
token,
|
|
Command(Request(
|
|
SourceCell,
|
|
new Vector3(12f, 18f, 7f)) with
|
|
{
|
|
Flags = PhysicsSetPositionFlags.RandomScatter,
|
|
ScatterRadiusX = 1f,
|
|
ScatterRadiusY = 1f,
|
|
ScatterAttempts = uint.MaxValue,
|
|
}));
|
|
|
|
Assert.Equal(RuntimeSetPositionStatus.Rejected, rejected.Status);
|
|
Assert.Equal(PhysicsSetPositionError.InvalidArguments, rejected.Error);
|
|
Assert.Equal(0, randomDraws);
|
|
Assert.False(lifetime.Physics.SetPosition
|
|
.TryGetPreparedMoverSphereCount(record, out _));
|
|
Assert.True(lifetime.Physics.SetPosition.TryGetAwaitingPreparationToken(
|
|
record,
|
|
out RuntimeEntityPlacementToken retry));
|
|
Assert.Equal(token, retry);
|
|
}
|
|
|
|
[Fact]
|
|
public void BodylessWithdrawalCancellationDoesNotInventBodyOrProjection()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001123u, 1);
|
|
RuntimeEntityPlacementToken token = lifetime.Physics.SetPosition
|
|
.BeginAcceptedPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
Assert.True(token.IsValid);
|
|
|
|
Assert.True(lifetime.Physics.SetPosition.Cancel(
|
|
record,
|
|
publishWithdrawal: true));
|
|
|
|
Assert.Null(record.PhysicsBody);
|
|
Assert.False(lifetime.Physics.SetPosition.TryPeekProjection(out _));
|
|
RuntimeSetPositionOwnershipSnapshot ownership = lifetime.Physics
|
|
.SetPosition.CaptureOwnership();
|
|
Assert.Equal(0, ownership.ActiveOperationCount);
|
|
Assert.Equal(0, ownership.PendingProjectionAcknowledgementCount);
|
|
Assert.True(ownership.IndexesConsistent);
|
|
}
|
|
|
|
[Fact]
|
|
public void ZeroExpectedVelocityPreservesBeginVersionAcrossInterveningVector()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001124u, 1);
|
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
|
|
RuntimeEntityPlacementToken token = lifetime.Physics.SetPosition
|
|
.BeginAcceptedPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
lifetime.Entities.AdvanceVectorAuthority(record);
|
|
body.set_velocity(new Vector3(-2f, 0f, 0f));
|
|
engine.TransitionCellCollisionTestHook =
|
|
(transition, phase, _, observed) =>
|
|
{
|
|
if (phase is TransitionCellCollisionPhase.Environment)
|
|
transition.CollisionInfo.SetCollisionNormal(Vector3.UnitX);
|
|
return observed;
|
|
};
|
|
|
|
RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition
|
|
.SubmitPreparedPlacement(
|
|
token,
|
|
Command(Request(
|
|
SourceCell,
|
|
new Vector3(14f, 18f, 7f))));
|
|
|
|
Assert.Equal(
|
|
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
|
|
outcome.Status);
|
|
Assert.Equal(new Vector3(-2f, 0f, 0f), body.Velocity);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
outcome.Projection));
|
|
}
|
|
|
|
[Fact]
|
|
public void CanonicalCommitReplacesDerivedBitsAndPublishesSlopeNormal()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001125u, 1);
|
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
|
|
body.TransientState |= TransientStateFlags.Sliding
|
|
| TransientStateFlags.StationaryStop
|
|
| TransientStateFlags.StationaryStuck
|
|
| TransientStateFlags.WaterContact;
|
|
body.SlidingNormal = Vector3.UnitY;
|
|
Vector3 slope = Vector3.Normalize(new Vector3(0.2f, 0f, 1f));
|
|
engine.TransitionCellCollisionTestHook =
|
|
(transition, phase, _, observed) =>
|
|
{
|
|
if (phase is TransitionCellCollisionPhase.Environment)
|
|
{
|
|
transition.CollisionInfo.SetContactPlane(
|
|
new Plane(slope, 0f),
|
|
SourceCell);
|
|
transition.CollisionInfo.FramesStationaryFall = 1;
|
|
}
|
|
return observed;
|
|
};
|
|
|
|
RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(Request(
|
|
SourceCell,
|
|
new Vector3(15f, 18f, 7f))));
|
|
|
|
Assert.Equal(
|
|
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
|
|
outcome.Status);
|
|
Assert.Equal(slope, body.GroundNormal);
|
|
Assert.False(body.TransientState.HasFlag(TransientStateFlags.Sliding));
|
|
Assert.Equal(Vector3.Zero, body.SlidingNormal);
|
|
Assert.True(body.TransientState.HasFlag(
|
|
TransientStateFlags.StationaryFall));
|
|
Assert.False(body.TransientState.HasFlag(
|
|
TransientStateFlags.StationaryStop));
|
|
Assert.False(body.TransientState.HasFlag(
|
|
TransientStateFlags.StationaryStuck));
|
|
Assert.False(body.TransientState.HasFlag(
|
|
TransientStateFlags.WaterContact));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
outcome.Projection));
|
|
}
|
|
|
|
[Fact]
|
|
public void ReentrantNewPlacementConvertsPublishedTokenToOrderedDiscard()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001004u, 1);
|
|
_ = AttachBody(lifetime, record, SourceCell);
|
|
RuntimeEntityPlacementToken replacement = default;
|
|
var observer = new PlacementObserver(delta =>
|
|
{
|
|
if (delta.Placement.Kind is RuntimePlacementProjectionKind.Place)
|
|
{
|
|
replacement = lifetime.Physics.SetPosition
|
|
.BeginAcceptedPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
}
|
|
});
|
|
using IDisposable subscription = lifetime.Events.SubscribePlacement(observer);
|
|
|
|
RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(Request(SourceCell, new Vector3(13f, 18f, 7f))));
|
|
|
|
Assert.True(replacement.IsValid);
|
|
Assert.Equal(2, observer.Deltas.Count);
|
|
Assert.Equal(RuntimePlacementProjectionKind.Place,
|
|
observer.Deltas[0].Placement.Kind);
|
|
Assert.Equal(RuntimePlacementProjectionKind.Discard,
|
|
observer.Deltas[1].Placement.Kind);
|
|
Assert.Equal(outcome.Projection.TokenSequence(),
|
|
observer.Deltas[1].Placement.Token.TokenSequence());
|
|
Assert.False(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
outcome.Projection));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
observer.Deltas[1].Placement.Token));
|
|
lifetime.Physics.SetPosition.Forget(
|
|
record,
|
|
releasePreparedMover: true);
|
|
Assert.True(lifetime.Physics.SetPosition.CaptureOwnership().IsConverged);
|
|
}
|
|
|
|
[Fact]
|
|
public void ReentrantGroundEdgePlacementCannotBeCancelledByDisplacedOperation()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
engine.TransitionCellCollisionTestHook =
|
|
(transition, phase, _, observed) =>
|
|
{
|
|
if (phase == TransitionCellCollisionPhase.Environment)
|
|
{
|
|
transition.CollisionInfo.SetContactPlane(
|
|
new Plane(Vector3.UnitZ, 0f),
|
|
SourceCell);
|
|
}
|
|
return observed;
|
|
};
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001042u, 1);
|
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
|
|
engine.ShadowObjects.Register(
|
|
record.Key!.Value.LocalEntityId,
|
|
0x01000001u,
|
|
body.Position,
|
|
body.Orientation,
|
|
0.48f,
|
|
0f,
|
|
0f,
|
|
SourceLandblock,
|
|
seedCellId: SourceCell,
|
|
isStatic: false);
|
|
RuntimeEntityPlacementToken replacement = default;
|
|
var remote = new ReentrantRemotePlacement(body)
|
|
{
|
|
CellId = SourceCell,
|
|
OnHitGround = () =>
|
|
{
|
|
replacement = lifetime.Physics.SetPosition
|
|
.BeginAcceptedPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
},
|
|
};
|
|
lifetime.Entities.SetRemoteMotion(record, remote);
|
|
|
|
RuntimeSetPositionOutcome displaced = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(Request(SourceCell, new Vector3(13f, 18f, 7f))));
|
|
|
|
Assert.Equal(RuntimeSetPositionStatus.Cancelled, displaced.Status);
|
|
Assert.True(replacement.IsValid);
|
|
RuntimeSetPositionOwnershipSnapshot ownership =
|
|
lifetime.Physics.SetPosition.CaptureOwnership();
|
|
Assert.Equal(1, ownership.ActiveOperationCount);
|
|
Assert.Equal(1, ownership.AwaitingPreparationCount);
|
|
Assert.Equal(13f, body.Position.X);
|
|
Assert.Equal(18f, body.Position.Y);
|
|
Assert.Equal(SourceCell, record.FullCellId);
|
|
Assert.True(body.InContact);
|
|
Assert.True(body.OnWalkable);
|
|
Assert.True(lifetime.Physics.IsSpatialRoot(record));
|
|
ShadowEntry shadow = Assert.Single(
|
|
engine.ShadowObjects.AllEntriesForDebug());
|
|
Assert.Equal(body.Position, shadow.Position);
|
|
|
|
RuntimeSetPositionOutcome retained = lifetime.Physics.SetPosition
|
|
.SubmitPreparedPlacement(
|
|
replacement,
|
|
Command(Request(
|
|
SourceCell,
|
|
new Vector3(14f, 18f, 7f))));
|
|
Assert.Equal(
|
|
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
|
|
retained.Status);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
retained.Projection));
|
|
lifetime.Physics.SetPosition.Forget(
|
|
record,
|
|
releasePreparedMover: true);
|
|
Assert.True(lifetime.Physics.SetPosition.CaptureOwnership().IsConverged);
|
|
}
|
|
|
|
/// <summary>
|
|
/// F1 regression: from within the ground-edge HitGround callback, drive
|
|
/// an explicit cancel-then-begin for the SAME entity. Because the
|
|
/// Operation pool is LIFO, this retires the outer, still-executing
|
|
/// operation and immediately rents that EXACT instance back out for the
|
|
/// new ("recycled") operation - a strictly more adversarial recycle than
|
|
/// ReentrantGroundEdgePlacementCannotBeCancelledByDisplacedOperation's
|
|
/// begin-only chain (there, nothing had retired the outer operation to
|
|
/// the pool by the time the reentrant Begin ran, so it always drew a
|
|
/// different or fresh instance). CommitCanonical must still feed the
|
|
/// ORIGINAL (pre-callback) PreviousContact/PreviousOnWalkable into
|
|
/// HandleSetPositionCollisionReports, not whatever the recycled instance
|
|
/// now holds for the unrelated new operation. This is observable: the
|
|
/// environment-collision report only fires at all when
|
|
/// (!previousOnWalkable && body.OnWalkable) - the recycled
|
|
/// instance's PreviousOnWalkable (captured from the body's ALREADY-
|
|
/// landed post-transition state) would read true instead of the
|
|
/// original false, silently suppressing the report if the operation's
|
|
/// live (potentially repurposed) fields were read instead of hoisted
|
|
/// locals.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ReentrantCancelThenBeginRecyclesInstanceButCollisionReportUsesPreCallbackValues()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
engine.TransitionCellCollisionTestHook =
|
|
(transition, phase, _, observed) =>
|
|
{
|
|
if (phase == TransitionCellCollisionPhase.Environment)
|
|
{
|
|
transition.CollisionInfo.SetContactPlane(
|
|
new Plane(Vector3.UnitZ, 0f),
|
|
SourceCell);
|
|
}
|
|
return observed;
|
|
};
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001046u, 1);
|
|
PhysicsBody body = AttachBody(
|
|
lifetime,
|
|
record,
|
|
SourceCell,
|
|
PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions);
|
|
var collisionObserver = new CollisionReportObserver();
|
|
using IDisposable collisionSubscription = lifetime.Physics
|
|
.CollisionReports.Subscribe(collisionObserver);
|
|
|
|
RuntimeEntityPlacementToken recycled = default;
|
|
var remote = new ReentrantRemotePlacement(body)
|
|
{
|
|
CellId = SourceCell,
|
|
OnHitGround = () =>
|
|
{
|
|
lifetime.Physics.SetPosition.Cancel(
|
|
record,
|
|
publishWithdrawal: false);
|
|
recycled = lifetime.Physics.SetPosition.BeginAcceptedPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
},
|
|
};
|
|
lifetime.Entities.SetRemoteMotion(record, remote);
|
|
|
|
_ = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(Request(SourceCell, new Vector3(13f, 18f, 7f))));
|
|
|
|
Assert.True(recycled.IsValid);
|
|
Assert.True(body.OnWalkable);
|
|
RuntimeCollisionReport report = Assert.Single(collisionObserver.Reports);
|
|
Assert.Equal(RuntimeCollisionReportKind.EnvironmentCollision, report.Kind);
|
|
Assert.False(report.RecipientWasInContact);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Round 3 pinned regression: the coordinator's precise reachability
|
|
/// trace for the Cancel-path recycle hazard. <c>Cancel(record, bool)</c>
|
|
/// creates a withdrawal operation and installs it at this entity's key,
|
|
/// then calls <c>PublishPlacement(cancelledOld)</c> to notify observers
|
|
/// that the still-pending Place projection has been superseded by a
|
|
/// Discard - a synchronous dispatch that can reach an arbitrary
|
|
/// <see cref="IRuntimePlacementObserver"/> subscriber, which here
|
|
/// reentrantly calls <c>BeginAcceptedPlacement</c> for the SAME entity.
|
|
/// Because the Operation pool is LIFO and
|
|
/// <c>BeginAcceptedPlacementCore</c> rents only after retiring, that
|
|
/// reentrant Begin retires the withdrawal operation <c>Cancel</c> is
|
|
/// mid-way through publishing for and rents the exact same physical
|
|
/// instance right back out for a brand-new "inner" operation. Before
|
|
/// round 3's captured-token-vs-fresh-lookup conversion, <c>Cancel</c>'s
|
|
/// post-publish check read <c>IsCurrent(operation)</c> off that live
|
|
/// (now-repurposed) reference - a tautology that would have let the
|
|
/// outer withdrawal proceed to publish a SECOND (Withdraw) projection
|
|
/// stamped with the inner operation's state, corrupting a Begin-only
|
|
/// operation that never asked for a pending projection. The fix
|
|
/// captures <c>operation.Token</c> into a local BEFORE
|
|
/// <c>PublishPlacement</c> runs and re-verifies it via a fresh
|
|
/// <c>IsCurrentByToken</c> lookup afterward, so the stale reference is
|
|
/// detected and the outer call returns without publishing anything
|
|
/// beyond the Discard.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ReentrantBeginDuringCancelPublishCannotBeOverwrittenByOuterWithdraw()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001250u, 1);
|
|
_ = AttachBody(lifetime, record, SourceCell);
|
|
RuntimeSetPositionOutcome pending = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(Request(SourceCell, new Vector3(18f, 18f, 7f))));
|
|
Assert.Equal(
|
|
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
|
|
pending.Status);
|
|
|
|
RuntimeEntityPlacementToken inner = default;
|
|
var observer = new PlacementObserver(delta =>
|
|
{
|
|
if (delta.Placement.Kind is RuntimePlacementProjectionKind.Discard)
|
|
{
|
|
inner = lifetime.Physics.SetPosition.BeginAcceptedPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
}
|
|
});
|
|
using IDisposable subscription = lifetime.Events.SubscribePlacement(observer);
|
|
|
|
bool removed = lifetime.Physics.SetPosition.Cancel(
|
|
record,
|
|
publishWithdrawal: true);
|
|
|
|
Assert.True(removed);
|
|
Assert.True(inner.IsValid);
|
|
// Only the Discard (for the pending Place this Cancel superseded)
|
|
// should have published - a stale-reference bug would have added a
|
|
// second (Withdraw) delta stamped with `inner`'s state.
|
|
RuntimePlacementDelta delta = Assert.Single(observer.Deltas);
|
|
Assert.Equal(RuntimePlacementProjectionKind.Discard, delta.Placement.Kind);
|
|
// `inner` must still be exactly what BeginAcceptedPlacement handed
|
|
// back - untouched by the outer Cancel's withdrawal publish.
|
|
Assert.True(lifetime.Physics.SetPosition.IsPlacementCurrent(inner));
|
|
RuntimeSetPositionOwnershipSnapshot ownership =
|
|
lifetime.Physics.SetPosition.CaptureOwnership();
|
|
Assert.Equal(1, ownership.ActiveOperationCount);
|
|
Assert.Equal(1, ownership.AwaitingPreparationCount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Round 3 CancelCore-shape regression: from within the ground-edge
|
|
/// HitGround callback, recycle the SAME operation instance
|
|
/// (cancel-then-begin, LIFO pool) for a brand-new "inner" operation on
|
|
/// the same entity, AND advance the record's PlacementCommitVersion a
|
|
/// second time (exactly what a completed nested commit would also have
|
|
/// produced) so CommitCanonical's post-callback
|
|
/// IsCanonicalPlacementCommitCurrent check fails for the OUTER commit.
|
|
/// SubmitPreparedPlacementCore's own failure branch then calls
|
|
/// <c>PublishCancellation(CancelCore(token.Entity, token))</c> using
|
|
/// `token` - the outer caller's own (now-stale) captured token. Before
|
|
/// round 3, <c>CancelCore(Operation expected, ...)</c> compared
|
|
/// `ReferenceEquals(current, expected)`; with the physical instance
|
|
/// recycled for `inner`, that check would have found the SAME instance
|
|
/// at this key and retired/cancelled it out from under the still-active
|
|
/// inner operation - the "worse outcome: cancelling the newer
|
|
/// operation" the reviewer flagged. The Token-keyed
|
|
/// <c>CancelCore(key, expectedToken)</c> must instead see
|
|
/// `current.Token != expectedToken` and no-op, leaving `inner`
|
|
/// untouched.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ReentrantCancelThenBeginDuringCommitFailureLeavesInnerOperationUncancelled()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
engine.TransitionCellCollisionTestHook =
|
|
(transition, phase, _, observed) =>
|
|
{
|
|
if (phase == TransitionCellCollisionPhase.Environment)
|
|
{
|
|
transition.CollisionInfo.SetContactPlane(
|
|
new Plane(Vector3.UnitZ, 0f),
|
|
SourceCell);
|
|
}
|
|
return observed;
|
|
};
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001251u, 1);
|
|
PhysicsBody body = AttachBody(
|
|
lifetime,
|
|
record,
|
|
SourceCell,
|
|
PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions);
|
|
|
|
RuntimeEntityPlacementToken inner = default;
|
|
var remote = new ReentrantRemotePlacement(body)
|
|
{
|
|
CellId = SourceCell,
|
|
OnHitGround = () =>
|
|
{
|
|
lifetime.Physics.SetPosition.Cancel(
|
|
record,
|
|
publishWithdrawal: false);
|
|
// Simulate a nested commit completing for `inner` (without
|
|
// needing a full nested SetPosition round-trip): advance
|
|
// PlacementCommitVersion BEFORE `inner`'s operation is
|
|
// created so `inner` snapshots the already-advanced value
|
|
// and stays internally self-consistent, while the OUTER
|
|
// commit's `canonicalCommitVersion` (captured before this
|
|
// callback ran) now mismatches the record.
|
|
lifetime.Entities.AdvancePlacementCommit(record);
|
|
inner = lifetime.Physics.SetPosition.BeginAcceptedPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
},
|
|
};
|
|
lifetime.Entities.SetRemoteMotion(record, remote);
|
|
|
|
RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(Request(SourceCell, new Vector3(13f, 18f, 7f))));
|
|
|
|
Assert.Equal(RuntimeSetPositionStatus.Cancelled, outcome.Status);
|
|
Assert.True(inner.IsValid);
|
|
// The bug this pins: a reference-based CancelCore would retire the
|
|
// recycled instance (now serving `inner`) out from under it.
|
|
// Confirm `inner` is still the live, current operation for this
|
|
// entity.
|
|
Assert.True(lifetime.Physics.SetPosition.IsPlacementCurrent(inner));
|
|
RuntimeSetPositionOwnershipSnapshot ownership =
|
|
lifetime.Physics.SetPosition.CaptureOwnership();
|
|
Assert.Equal(1, ownership.ActiveOperationCount);
|
|
Assert.Equal(1, ownership.AwaitingPreparationCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void RetrySnapshotPreservesOrderWhenObserverAcknowledgesTwoPendingTokens()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord first = CreateRecord(lifetime, 0x70001040u, 1);
|
|
RuntimeEntityRecord second = CreateRecord(lifetime, 0x70001041u, 1);
|
|
_ = AttachBody(lifetime, first, SourceCell);
|
|
_ = AttachBody(lifetime, second, SourceCell);
|
|
bool acknowledgeRetries = false;
|
|
var retryOrder = new List<ulong>();
|
|
var observer = new PlacementObserver(delta =>
|
|
{
|
|
if (!acknowledgeRetries)
|
|
return;
|
|
retryOrder.Add(delta.Placement.Token.Sequence);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
delta.Placement.Token));
|
|
});
|
|
using IDisposable subscription = lifetime.Events.SubscribePlacement(observer);
|
|
RuntimeSetPositionOutcome firstOutcome = lifetime.Physics.SetPosition.Apply(
|
|
first,
|
|
first.PositionAuthorityVersion,
|
|
Command(Request(SourceCell, new Vector3(14f, 18f, 7f))));
|
|
RuntimeSetPositionOutcome secondOutcome = lifetime.Physics.SetPosition.Apply(
|
|
second,
|
|
second.PositionAuthorityVersion,
|
|
Command(Request(SourceCell, new Vector3(16f, 18f, 7f))));
|
|
|
|
acknowledgeRetries = true;
|
|
lifetime.Physics.SetPosition.RetryPendingProjections();
|
|
|
|
Assert.Equal(
|
|
[firstOutcome.Projection.Sequence, secondOutcome.Projection.Sequence],
|
|
retryOrder);
|
|
Assert.Equal(0, lifetime.Physics.CaptureOwnership()
|
|
.PendingSetPositionHostAcknowledgementCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void ThrowingObserverDoesNotRollBackCanonicalCommitAndRetryCanAck()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001043u, 1);
|
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
|
|
using IDisposable throwing = lifetime.Events.SubscribePlacement(
|
|
new ThrowingPlacementObserver());
|
|
|
|
RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(Request(SourceCell, new Vector3(17f, 18f, 7f))));
|
|
|
|
Assert.Equal(
|
|
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
|
|
outcome.Status);
|
|
Assert.Equal(new Vector3(17f, 18f, 7f), body.Position);
|
|
Assert.Equal(1, lifetime.Events.DispatchFailureCount);
|
|
Assert.Equal(1, lifetime.Physics.CaptureOwnership()
|
|
.PendingSetPositionHostAcknowledgementCount);
|
|
|
|
throwing.Dispose();
|
|
var healthy = new PlacementObserver();
|
|
using IDisposable subscription = lifetime.Events.SubscribePlacement(healthy);
|
|
lifetime.Physics.SetPosition.RetryPendingProjections();
|
|
Assert.Equal(outcome.Projection,
|
|
Assert.Single(healthy.Deltas).Placement.Token);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
outcome.Projection));
|
|
}
|
|
|
|
[Fact]
|
|
public void ForgetRevisesPendingTokenAndRetryPublishesDiscardBeforeAck()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001117u, 1);
|
|
_ = AttachBody(lifetime, record, SourceCell);
|
|
var observer = new PlacementObserver();
|
|
using IDisposable subscription = lifetime.Events.SubscribePlacement(observer);
|
|
RuntimeSetPositionOutcome pending = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(Request(SourceCell, new Vector3(17f, 17f, 7f))));
|
|
|
|
RuntimePlacementCancellationReceipt cancellation =
|
|
lifetime.Physics.SetPosition.Forget(record);
|
|
|
|
Assert.True(cancellation.IsValid);
|
|
Assert.Single(observer.Deltas);
|
|
Assert.False(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
pending.Projection));
|
|
lifetime.Physics.SetPosition.RetryPendingProjections();
|
|
RuntimePlacementProjectionSnapshot discard = observer.Deltas[^1].Placement;
|
|
Assert.Equal(RuntimePlacementProjectionKind.Discard, discard.Kind);
|
|
Assert.Equal(pending.Projection.Sequence, discard.Token.Sequence);
|
|
Assert.True(discard.Token.Revision > pending.Projection.Revision);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
discard.Token));
|
|
}
|
|
|
|
[Fact]
|
|
public void ReplacementRetainsUnacknowledgedLostWithdrawalUntilPreparedCommandCanPlace()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x7000111Au, 1);
|
|
_ = AttachBody(lifetime, record, SourceCell);
|
|
using IDisposable throwing = lifetime.Events.SubscribePlacement(
|
|
new ThrowingPlacementObserver());
|
|
RuntimeSetPositionOutcome original = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(CrossLandblockRequest()));
|
|
Assert.Equal(RuntimeSetPositionStatus.DeferredCell, original.Status);
|
|
|
|
RuntimeEntityPlacementToken replacement = lifetime.Physics.SetPosition
|
|
.BeginAcceptedPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
Assert.True(replacement.IsValid);
|
|
RuntimeSetPositionOutcome waiting = lifetime.Physics.SetPosition
|
|
.SubmitPreparedPlacement(
|
|
replacement,
|
|
Command(Request(SourceCell, new Vector3(18f, 18f, 7f))));
|
|
Assert.Equal(RuntimeSetPositionStatus.DeferredCell, waiting.Status);
|
|
Assert.Equal(original.Projection, waiting.Projection);
|
|
Assert.Equal(0u, record.FullCellId);
|
|
|
|
throwing.Dispose();
|
|
var healthy = new PlacementObserver();
|
|
using IDisposable subscription = lifetime.Events.SubscribePlacement(healthy);
|
|
lifetime.Physics.SetPosition.RetryPendingProjections();
|
|
RuntimePlacementProjectionSnapshot withdrawal =
|
|
Assert.Single(healthy.Deltas).Placement;
|
|
Assert.Equal(RuntimePlacementProjectionKind.Withdraw, withdrawal.Kind);
|
|
Assert.Equal(original.Projection, withdrawal.Token);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
withdrawal.Token));
|
|
|
|
Assert.Equal(2, healthy.Deltas.Count);
|
|
RuntimePlacementProjectionSnapshot placed = healthy.Deltas[1].Placement;
|
|
Assert.Equal(RuntimePlacementProjectionKind.Place, placed.Kind);
|
|
Assert.Equal(SourceCell, record.FullCellId);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
placed.Token));
|
|
}
|
|
|
|
/// <summary>
|
|
/// C4 route 4b-1 review, the decisive case. <c>ParkDeferred</c> withdraws
|
|
/// the entity from the world (<c>InWorld = false</c>, object clock
|
|
/// suspended, canonical residency dropped). The merge-time
|
|
/// <c>Forget</c> that every accepted Position performs
|
|
/// (<c>RuntimeEntityObjectLifetime.TryApplyPosition</c>) then removes the
|
|
/// operation and restores NONE of that, so the entity is left invisible
|
|
/// AND intangible with nothing left that could ever wake it.
|
|
///
|
|
/// <para>Retail cannot reach that state: <c>CPhysicsObj::SetPositionInternal</c>
|
|
/// @0x00515BD0 commits the destination pose via <c>store_position</c>
|
|
/// @0x00515CE2 and registers the object with
|
|
/// <c>CObjectMaint::GotoLostCell</c> @0x00515CF2 / @0x00508210, and the
|
|
/// ONLY remover of that registration is
|
|
/// <c>CObjectMaint::InitObjCell</c> @0x00508260, which drains the lost
|
|
/// list on cell load and calls <c>CPhysicsObj::reenter_visibility</c>
|
|
/// @0x00508296 / @0x00516250 for every object in it. A retail lost object
|
|
/// is never silently un-registered.</para>
|
|
/// </summary>
|
|
[Fact]
|
|
public void CancellingWakeableParkLeavesEntityWithdrawnWithNothingToWakeIt()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x7000411Bu, 1);
|
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
|
|
|
|
Assert.True(body.InWorld);
|
|
Assert.True(record.ObjectClock.IsActive);
|
|
Assert.Equal(SourceCell, record.FullCellId);
|
|
|
|
RuntimeSetPositionOutcome parked = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(CrossLandblockRequest()));
|
|
|
|
Assert.Equal(RuntimeSetPositionStatus.DeferredCell, parked.Status);
|
|
Assert.True(lifetime.Physics.SetPosition.IsDeferred(record));
|
|
Assert.False(body.InWorld);
|
|
Assert.False(record.ObjectClock.IsActive);
|
|
Assert.Equal(0u, record.FullCellId);
|
|
Vector3 parkedPosition = body.Position;
|
|
|
|
// The merge-time cancel every accepted Position performs.
|
|
_ = lifetime.Physics.SetPosition.Forget(
|
|
record,
|
|
restoreCancelledPark: true);
|
|
|
|
// The park is gone (the shipped newer-Position-cancels-the-park
|
|
// invariant, pinned by
|
|
// NewerPositionPickupAndParentEachCancelExactLostOperation) - so the
|
|
// entity MUST have been rolled back, or nothing is left to wake it.
|
|
Assert.False(lifetime.Physics.SetPosition.IsDeferred(record));
|
|
Assert.True(
|
|
body.InWorld,
|
|
"cancelled park left the entity withdrawn: InWorld=false");
|
|
Assert.True(
|
|
record.ObjectClock.IsActive,
|
|
"cancelled park left the object clock suspended");
|
|
Assert.True(
|
|
record.FullCellId != 0u,
|
|
"cancelled park left the entity without canonical residency");
|
|
Assert.True(
|
|
lifetime.Physics.IsSpatialRoot(record),
|
|
"cancelled park left the entity out of the physics workset");
|
|
Assert.Equal(
|
|
TransientStateFlags.Active,
|
|
body.TransientState & TransientStateFlags.Active);
|
|
|
|
// The pose committed at park time STANDS - retail's store_position
|
|
// @0x00515CE2 commits the destination on the lost-cell branch and
|
|
// never un-commits it. Residency follows the body's committed cell so
|
|
// the two cannot disagree.
|
|
Assert.Equal(parkedPosition, body.Position);
|
|
Assert.Equal(body.CellPosition.ObjCellId, record.FullCellId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// C5b review L5 (2026-08-05). C5b kept the merge's
|
|
/// <c>beforeCell != canonical.FullCellId ? Rebucketed : Updated</c>
|
|
/// ternary alive on the argument that the
|
|
/// <c>Forget(restoreCancelledPark: true)</c> above it can produce a real
|
|
/// 0 -> N cell edge INSIDE <c>TryApplyPosition</c>. Every
|
|
/// <c>restoreCancelledPark</c> test until now called <c>Forget</c>
|
|
/// directly, so the retained arm had zero coverage through the production
|
|
/// entry point and the argument was unpinned. This drives the real merge.
|
|
///
|
|
/// <para>The wire cell is deliberately the SOURCE cell while the park's
|
|
/// committed body cell is the DESTINATION: the restored residency — and
|
|
/// therefore the <c>Rebucketed</c> delta's value — can only have come
|
|
/// from the park rollback, never from the wire packet, which C5b withholds
|
|
/// (<c>RuntimeSetPositionState.cs</c>'s <c>RestoreParkWithdrawal</c> writes
|
|
/// <c>SetFullCell</c> only while <c>FullCellId == 0u</c>, which is exactly
|
|
/// the state <c>ParkDeferred</c> left behind).</para>
|
|
/// </summary>
|
|
[Fact]
|
|
public void AcceptedPositionCancellingWakeableParkPublishesRebucketedThroughTheMerge()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
var generation = new RuntimeGenerationToken(1UL);
|
|
lifetime.BindEventContext(() => generation, static () => 1UL);
|
|
const uint guid = 0x7000411Fu;
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, guid, 1);
|
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
|
|
|
|
RuntimeSetPositionOutcome parked = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(CrossLandblockRequest()));
|
|
Assert.Equal(RuntimeSetPositionStatus.DeferredCell, parked.Status);
|
|
Assert.Equal(0u, record.FullCellId);
|
|
Assert.Equal(DestinationCell, body.CellPosition.ObjCellId);
|
|
|
|
var observer = new EntityObserver();
|
|
using IDisposable subscription = lifetime.Events.Subscribe(observer);
|
|
|
|
Assert.True(lifetime.TryApplyPosition(
|
|
new WorldSession.EntityPositionUpdate(
|
|
guid,
|
|
new CreateObject.ServerPosition(
|
|
SourceCell, 11f, 21f, 7f, 1f, 0f, 0f, 0f),
|
|
Velocity: null,
|
|
PlacementId: null,
|
|
IsGrounded: true,
|
|
InstanceSequence: 1,
|
|
PositionSequence: 2,
|
|
TeleportSequence: 0,
|
|
ForcePositionSequence: 0),
|
|
isLocalPlayer: false,
|
|
forcePositionRotation: null,
|
|
currentLocalVelocity: null,
|
|
acknowledgeProjection: null,
|
|
out PositionTimestampDisposition disposition,
|
|
out _,
|
|
out _));
|
|
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
|
|
|
|
// The park rolled back inside the merge: 0 -> the body's committed
|
|
// destination cell, which is NOT the wire cell.
|
|
Assert.Equal(DestinationCell, record.FullCellId);
|
|
Assert.NotEqual(SourceCell, record.FullCellId);
|
|
Assert.True(body.InWorld);
|
|
|
|
// THE assertion the retained ternary owns. Collapsing it to a constant
|
|
// Updated turns this red; the wire-cell withhold cannot rescue it,
|
|
// because the edge came from the rollback.
|
|
RuntimeEntityDelta delta = Assert.Single(
|
|
observer.Deltas,
|
|
d => d.Entity.Identity.ServerGuid == guid);
|
|
Assert.Equal(RuntimeEntityChange.Rebucketed, delta.Change);
|
|
Assert.Equal(DestinationCell, delta.Entity.CellId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The sibling cancel entry point. Route 2's controller and route 4b's
|
|
/// remote controller both cancel a <c>DeferredCell</c> park through
|
|
/// <c>ForgetExactPlacement</c>, not through <c>Forget</c>, so the rollback
|
|
/// has to live at the shared <c>CancelCore</c> layer or the identical
|
|
/// stranded-entity hole stays open on exactly the routes that produce it.
|
|
/// </summary>
|
|
/// <summary>
|
|
/// The canonical rollback above is only HALF of what the park's Withdraw
|
|
/// receipt removed. Its presentation half - the graphical bucket, the
|
|
/// projection-visibility sinks, plugin world state/events, the effect-pose
|
|
/// registry, the local-player shadow - was performed by the host sink that
|
|
/// consumed that receipt, and <c>RestoreParkWithdrawal</c> cannot reach any
|
|
/// of it. Before this receipt existed the only thing that re-installed it
|
|
/// was a LATER <c>Place</c>, which a remote that parks on its final
|
|
/// Position and then stops moving never receives: ACE stops broadcasting
|
|
/// for a stationary entity. The observable result was a fully simulated,
|
|
/// collidable, audible player that was absent from both the world render
|
|
/// and the radar for the rest of the session.
|
|
///
|
|
/// <para>So the cancel must publish the inverse receipt itself, on the one
|
|
/// ordered stream, and it must be acknowledge-only: the parked operation
|
|
/// is already removed and retired by <c>CancelCoreDeferred</c> when this
|
|
/// publishes.</para>
|
|
/// </summary>
|
|
[Fact]
|
|
public void CancellingWakeableParkPublishesTheWithdrawalRestorationReceipt()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x7000411Du, 1);
|
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
|
|
var placements = new PlacementObserver();
|
|
using IDisposable subscription =
|
|
lifetime.Events.SubscribePlacement(placements);
|
|
|
|
RuntimeSetPositionOutcome parked = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(CrossLandblockRequest()));
|
|
Assert.Equal(RuntimeSetPositionStatus.DeferredCell, parked.Status);
|
|
Assert.Equal(
|
|
RuntimePlacementProjectionKind.Withdraw,
|
|
Assert.Single(placements.Deltas).Placement.Kind);
|
|
|
|
_ = lifetime.Physics.SetPosition.Forget(
|
|
record,
|
|
restoreCancelledPark: true);
|
|
|
|
Assert.True(body.InWorld);
|
|
Assert.NotEqual(0u, record.FullCellId);
|
|
|
|
// Two receipts are pending, in canonical order: the cancelled park's
|
|
// own Discard at the lower sequence, then the restoration.
|
|
Assert.Equal(2, lifetime.Physics.SetPosition.PendingProjectionCount);
|
|
Assert.True(lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot head));
|
|
Assert.Equal(RuntimePlacementProjectionKind.Discard, head.Kind);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
head.Token));
|
|
|
|
Assert.True(lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot restoration));
|
|
Assert.Equal(
|
|
RuntimePlacementProjectionKind.WithdrawalRestored,
|
|
restoration.Kind);
|
|
Assert.Equal(record.Key, restoration.Token.Entity);
|
|
Assert.Equal(record.FullCellId, restoration.Token.ExactCellId);
|
|
Assert.Equal(body.Position, restoration.WorldPosition);
|
|
|
|
// The host's per-frame RetryPending pump is what delivers it, exactly
|
|
// as it delivers any receipt that was not the head when published.
|
|
placements.Deltas.Clear();
|
|
lifetime.Physics.SetPosition.RetryPendingProjections();
|
|
Assert.Equal(
|
|
restoration,
|
|
Assert.Single(placements.Deltas).Placement);
|
|
|
|
// Acknowledge-only: no operation backs it, and the stream drains.
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
restoration.Token));
|
|
Assert.Equal(
|
|
0,
|
|
lifetime.Physics.SetPosition.PendingProjectionCount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// AP-136's refusal arm stays exact: when the committed cell's prefix is
|
|
/// retiring, the rollback declines residency, and presentation must stay
|
|
/// withdrawn to match rather than showing an entity Runtime deliberately
|
|
/// left out of the world.
|
|
/// </summary>
|
|
[Fact]
|
|
public void CancellingParkIntoQuiescingPrefixPublishesNoRestorationReceipt()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x7000411Eu, 1);
|
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
|
|
|
|
RuntimeSetPositionOutcome parked = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(CrossLandblockRequest()));
|
|
Assert.Equal(RuntimeSetPositionStatus.DeferredCell, parked.Status);
|
|
// Round 4 (D6)'s window: the prefix was clean when the park was taken
|
|
// (so the withdrawal WAS captured) and starts retiring before the
|
|
// cancel lands, which is only reachable because a retained park's
|
|
// restore runs on a later packet.
|
|
uint parkedCell = body.CellPosition.ObjCellId;
|
|
_ = lifetime.Physics.BeginCollisionPrefixQuiescence(
|
|
(parkedCell & 0xFFFF0000u) | 0xFFFFu,
|
|
collisionGeneration: 1UL,
|
|
includeOutdoorCells: true);
|
|
int pendingBefore =
|
|
lifetime.Physics.SetPosition.PendingProjectionCount;
|
|
|
|
_ = lifetime.Physics.SetPosition.Forget(
|
|
record,
|
|
restoreCancelledPark: true);
|
|
|
|
Assert.Equal(0u, record.FullCellId);
|
|
// Only the cancelled park's Discard rewrote an existing entry; no new
|
|
// restoration receipt was appended.
|
|
Assert.Equal(
|
|
pendingBefore,
|
|
lifetime.Physics.SetPosition.PendingProjectionCount);
|
|
Assert.True(lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot head));
|
|
Assert.Equal(RuntimePlacementProjectionKind.Discard, head.Kind);
|
|
}
|
|
|
|
[Fact]
|
|
public void CancellingWakeableParkByExactTokenAlsoRestoresTheEntity()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x7000411Cu, 1);
|
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
|
|
|
|
RuntimeEntityPlacementToken token = lifetime.Physics.SetPosition
|
|
.BeginAcceptedPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
Assert.True(token.IsValid);
|
|
RuntimeSetPositionOutcome parked = lifetime.Physics.SetPosition
|
|
.SubmitPreparedPlacement(token, Command(CrossLandblockRequest()));
|
|
Assert.Equal(RuntimeSetPositionStatus.DeferredCell, parked.Status);
|
|
Assert.False(body.InWorld);
|
|
Assert.False(record.ObjectClock.IsActive);
|
|
Assert.Equal(0u, record.FullCellId);
|
|
|
|
_ = lifetime.Physics.SetPosition.ForgetExactPlacement(
|
|
token,
|
|
restoreCancelledPark: true);
|
|
|
|
Assert.False(lifetime.Physics.SetPosition.IsDeferred(record));
|
|
Assert.True(body.InWorld, "exact-token cancel left the entity withdrawn");
|
|
Assert.True(
|
|
record.ObjectClock.IsActive,
|
|
"exact-token cancel left the object clock suspended");
|
|
Assert.True(
|
|
record.FullCellId != 0u,
|
|
"exact-token cancel left the entity without canonical residency");
|
|
Assert.True(lifetime.Physics.IsSpatialRoot(record));
|
|
}
|
|
|
|
[Fact]
|
|
public void ReentrantNewerPositionDuringPickupDiscardSuppressesStalePickupDelta()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001118u, 1);
|
|
_ = AttachBody(lifetime, record, SourceCell);
|
|
RuntimeSetPositionOutcome pending = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(Request(SourceCell, new Vector3(17f, 16f, 7f))));
|
|
var entities = new EntityObserver();
|
|
using IDisposable entitySubscription = lifetime.Events.Subscribe(entities);
|
|
bool reentered = false;
|
|
RuntimeEventStamp discardStamp = default;
|
|
var placements = new PlacementObserver(delta =>
|
|
{
|
|
if (reentered
|
|
|| delta.Placement.Kind
|
|
is not RuntimePlacementProjectionKind.Discard)
|
|
{
|
|
return;
|
|
}
|
|
reentered = true;
|
|
discardStamp = delta.Stamp;
|
|
Assert.True(lifetime.TryApplyPosition(
|
|
new WorldSession.EntityPositionUpdate(
|
|
record.ServerGuid,
|
|
new CreateObject.ServerPosition(
|
|
SourceCell, 12f, 20f, 7f, 1f, 0f, 0f, 0f),
|
|
Velocity: null,
|
|
PlacementId: null,
|
|
IsGrounded: true,
|
|
InstanceSequence: record.Incarnation,
|
|
PositionSequence: 3,
|
|
TeleportSequence: 0,
|
|
ForcePositionSequence: 0),
|
|
isLocalPlayer: false,
|
|
forcePositionRotation: null,
|
|
currentLocalVelocity: null,
|
|
acknowledgeProjection: null,
|
|
out _,
|
|
out _,
|
|
out _));
|
|
});
|
|
using IDisposable placementSubscription =
|
|
lifetime.Events.SubscribePlacement(placements);
|
|
|
|
Assert.False(lifetime.TryApplyPickup(
|
|
new PickupEvent.Parsed(record.ServerGuid, 1, 2),
|
|
acknowledgeProjection: null,
|
|
out _));
|
|
|
|
Assert.True(reentered);
|
|
RuntimeEntityDelta only = Assert.Single(entities.Deltas);
|
|
// C5b (#275): the reentrant accepted Position publishes Updated, not
|
|
// Rebucketed — the steady-state merge is no longer a residency writer,
|
|
// so a wire cell arriving on a withdrawn (picked-up) record does not
|
|
// make it resident. Production commits the cell one step later, at the
|
|
// OnPosition prologue rebucket, which this Runtime-level fixture does
|
|
// not drive. The SUBJECT of this test is unchanged: exactly one delta,
|
|
// published by the reentrant Position, superseding the stale pickup
|
|
// discard.
|
|
Assert.Equal(RuntimeEntityChange.Updated, only.Change);
|
|
Assert.Equal(0u, record.FullCellId);
|
|
Assert.True(only.Stamp.Sequence > discardStamp.Sequence);
|
|
Assert.False(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
pending.Projection));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
Assert.Single(placements.Deltas).Placement.Token));
|
|
}
|
|
|
|
[Fact]
|
|
public void SyntheticWithdrawalAcknowledgementTerminatesOperation()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001044u, 1);
|
|
_ = AttachBody(lifetime, record, SourceCell);
|
|
var observer = new PlacementObserver();
|
|
using IDisposable subscription = lifetime.Events.SubscribePlacement(observer);
|
|
|
|
Assert.True(lifetime.Physics.SetPosition.Cancel(
|
|
record,
|
|
publishWithdrawal: true));
|
|
RuntimePlacementProjectionToken token = Assert.Single(observer.Deltas)
|
|
.Placement.Token;
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(token));
|
|
Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership()
|
|
.ActiveOperationCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void ReentrantBeginDuringDiscardCannotBeOverwrittenByOuterBegin()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001045u, 1);
|
|
_ = AttachBody(lifetime, record, SourceCell);
|
|
RuntimeSetPositionOutcome pending = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(Request(SourceCell, new Vector3(18f, 18f, 7f))));
|
|
RuntimeEntityPlacementToken inner = default;
|
|
var observer = new PlacementObserver(delta =>
|
|
{
|
|
if (delta.Placement.Kind is RuntimePlacementProjectionKind.Discard)
|
|
{
|
|
inner = lifetime.Physics.SetPosition.BeginAcceptedPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
}
|
|
});
|
|
using IDisposable subscription = lifetime.Events.SubscribePlacement(observer);
|
|
|
|
RuntimeEntityPlacementToken outer = lifetime.Physics.SetPosition
|
|
.BeginAcceptedPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
|
|
Assert.False(outer.IsValid);
|
|
Assert.True(inner.IsValid);
|
|
Assert.False(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
pending.Projection));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
Assert.Single(observer.Deltas).Placement.Token));
|
|
RuntimeSetPositionOutcome committed = lifetime.Physics.SetPosition
|
|
.SubmitPreparedPlacement(
|
|
inner,
|
|
Command(Request(SourceCell, new Vector3(19f, 18f, 7f))));
|
|
Assert.Equal(
|
|
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
|
|
committed.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public void CollisionRetirementRejectsActivePlacementBeforeResidentMutation()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001046u, 1);
|
|
_ = AttachBody(lifetime, record, SourceCell);
|
|
RuntimeEntityPlacementToken token = lifetime.Physics.SetPosition
|
|
.BeginAcceptedPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
lifetime.Physics.SetPosition.ParkCollisionResidents(
|
|
SourceLandblock,
|
|
includeOutdoorCells: true));
|
|
Assert.Equal(SourceCell, record.FullCellId);
|
|
Assert.True(lifetime.Physics.IsSpatialRoot(record));
|
|
RuntimeSetPositionOutcome submitted = lifetime.Physics.SetPosition
|
|
.SubmitPreparedPlacement(
|
|
token,
|
|
Command(Request(SourceCell, new Vector3(20f, 18f, 7f))));
|
|
|
|
Assert.Equal(
|
|
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
|
|
submitted.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public void CollisionRetirementRejectsHostAckPendingPlacementBeforeResidentMutation()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x7000111Bu, 1);
|
|
_ = AttachBody(lifetime, record, SourceCell);
|
|
RuntimeSetPositionOutcome pending = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(Request(SourceCell, new Vector3(20f, 19f, 7f))));
|
|
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
lifetime.Physics.SetPosition.ParkCollisionResidents(
|
|
SourceLandblock,
|
|
includeOutdoorCells: true));
|
|
Assert.Equal(SourceCell, record.FullCellId);
|
|
Assert.True(lifetime.Physics.IsSpatialRoot(record));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
pending.Projection));
|
|
}
|
|
|
|
[Fact]
|
|
public void CollisionRetirementInstallsEveryRootBeforeWithdrawObserverReentry()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord first = CreateRecord(lifetime, 0x7000111Du, 1);
|
|
RuntimeEntityRecord second = CreateRecord(lifetime, 0x7000111Eu, 1);
|
|
_ = AttachBody(lifetime, first, SourceCell);
|
|
_ = AttachBody(lifetime, second, SourceCell);
|
|
RuntimeEntityPlacementToken replacement = default;
|
|
RuntimeSetPositionOutcome submitted = default;
|
|
var observer = new PlacementObserver(delta =>
|
|
{
|
|
if (delta.Placement.Kind is RuntimePlacementProjectionKind.Withdraw
|
|
&& delta.Placement.Token.Entity == first.Key)
|
|
{
|
|
replacement = lifetime.Physics.SetPosition
|
|
.BeginAcceptedPlacement(
|
|
second,
|
|
second.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
submitted = lifetime.Physics.SetPosition
|
|
.SubmitPreparedPlacement(
|
|
replacement,
|
|
Command(Request(
|
|
SourceCell,
|
|
new Vector3(21f, 19f, 7f))));
|
|
}
|
|
});
|
|
using IDisposable subscription = lifetime.Events.SubscribePlacement(observer);
|
|
|
|
lifetime.Physics.SetPosition.ParkCollisionResidents(
|
|
SourceLandblock,
|
|
includeOutdoorCells: true);
|
|
|
|
Assert.True(replacement.IsValid);
|
|
Assert.Equal(RuntimeSetPositionStatus.DeferredCell, submitted.Status);
|
|
Assert.Equal(2, observer.Deltas.Count);
|
|
Assert.All(observer.Deltas, delta => Assert.Equal(
|
|
RuntimePlacementProjectionKind.Withdraw,
|
|
delta.Placement.Kind));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
observer.Deltas[0].Placement.Token));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
observer.Deltas[1].Placement.Token));
|
|
RuntimePlacementProjectionSnapshot placed = observer.Deltas[^1].Placement;
|
|
Assert.Equal(RuntimePlacementProjectionKind.Place, placed.Kind);
|
|
Assert.Equal(second.Key, placed.Token.Entity);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
placed.Token));
|
|
}
|
|
|
|
[Fact]
|
|
public void RootAndCurrentDirectChildOwnIndependentExactLostDeadlines()
|
|
{
|
|
var time = new ManualTimeProvider(DateTimeOffset.UnixEpoch);
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(
|
|
engine,
|
|
timeProvider: time);
|
|
RuntimeEntityRecord root = CreateRecord(lifetime, 0x70001005u, 3);
|
|
RuntimeEntityRecord child = CreateRecord(lifetime, 0x70001006u, 9);
|
|
_ = AttachBody(lifetime, root, SourceCell);
|
|
var relation = new ParentAttachmentRelation(
|
|
root.ServerGuid,
|
|
child.ServerGuid,
|
|
ParentLocation: 1,
|
|
PlacementId: 2,
|
|
ParentInstanceSequence: root.Incarnation,
|
|
ChildPositionSequence: 1);
|
|
lifetime.Entities.ParentAttachments.AcceptCreateObjectRelation(relation);
|
|
Assert.True(lifetime.Entities.ParentAttachments.CommitProjection(relation));
|
|
|
|
RuntimeSetPositionOutcome deferred = lifetime.Physics.SetPosition.Apply(
|
|
root,
|
|
root.PositionAuthorityVersion,
|
|
Command(CrossLandblockRequest()));
|
|
|
|
Assert.Equal(2, lifetime.Physics.CaptureOwnership().LostCellDeadlineCount);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
deferred.Projection));
|
|
time.Advance(TimeSpan.FromSeconds(26));
|
|
lifetime.Physics.SetPosition.TickLostCellDeadlines();
|
|
Assert.Equal(2, lifetime.Physics.CaptureOwnership()
|
|
.ExpiredLostCellCount);
|
|
|
|
var expired = new HashSet<RuntimeEntityKey>();
|
|
while (lifetime.Physics.SetPosition.TryDequeueExpiredLostCell(out var key))
|
|
expired.Add(key);
|
|
Assert.Equal(2, expired.Count);
|
|
Assert.Contains(root.Key!.Value, expired);
|
|
Assert.Contains(child.Key!.Value, expired);
|
|
Assert.Equal(0, lifetime.Physics.CaptureOwnership().LostCellDeadlineCount);
|
|
Assert.Equal(0, lifetime.Physics.CaptureOwnership()
|
|
.ExpiredLostCellCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExpiredRootDeletionDoesNotConsumeIndependentChildExpiry()
|
|
{
|
|
var time = new ManualTimeProvider(DateTimeOffset.UnixEpoch);
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine, timeProvider: time);
|
|
RuntimeEntityRecord root = CreateRecord(lifetime, 0x70001047u, 1);
|
|
RuntimeEntityRecord child = CreateRecord(lifetime, 0x70001048u, 1);
|
|
_ = AttachBody(lifetime, root, SourceCell);
|
|
CommitParent(lifetime, root, child);
|
|
RuntimeSetPositionOutcome deferred = lifetime.Physics.SetPosition.Apply(
|
|
root,
|
|
root.PositionAuthorityVersion,
|
|
Command(CrossLandblockRequest()));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
deferred.Projection));
|
|
time.Advance(TimeSpan.FromSeconds(26));
|
|
lifetime.Physics.SetPosition.TickLostCellDeadlines();
|
|
|
|
Assert.True(lifetime.Physics.SetPosition.TryDequeueExpiredLostCell(
|
|
out RuntimeEntityKey expiredRoot));
|
|
Assert.Equal(root.Key, expiredRoot);
|
|
lifetime.Physics.SetPosition.Forget(root);
|
|
Assert.True(lifetime.Physics.SetPosition.TryDequeueExpiredLostCell(
|
|
out RuntimeEntityKey expiredChild));
|
|
Assert.Equal(child.Key, expiredChild);
|
|
}
|
|
|
|
[Fact]
|
|
public void ChildPickupCancelsOnlyItsCapturedLostDeadline()
|
|
{
|
|
var time = new ManualTimeProvider(DateTimeOffset.UnixEpoch);
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine, timeProvider: time);
|
|
RuntimeEntityRecord root = CreateRecord(lifetime, 0x70001049u, 1);
|
|
RuntimeEntityRecord child = CreateRecord(lifetime, 0x7000104Au, 1);
|
|
_ = AttachBody(lifetime, root, SourceCell);
|
|
CommitParent(lifetime, root, child);
|
|
RuntimeSetPositionOutcome deferred = lifetime.Physics.SetPosition.Apply(
|
|
root,
|
|
root.PositionAuthorityVersion,
|
|
Command(CrossLandblockRequest()));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
deferred.Projection));
|
|
|
|
Assert.True(lifetime.TryApplyPickup(
|
|
new PickupEvent.Parsed(child.ServerGuid, 1, 2),
|
|
acknowledgeProjection: null,
|
|
out _));
|
|
time.Advance(TimeSpan.FromSeconds(26));
|
|
lifetime.Physics.SetPosition.TickLostCellDeadlines();
|
|
|
|
Assert.True(lifetime.Physics.SetPosition.TryDequeueExpiredLostCell(
|
|
out RuntimeEntityKey only));
|
|
Assert.Equal(root.Key, only);
|
|
Assert.False(lifetime.Physics.SetPosition.TryDequeueExpiredLostCell(out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void LostDeadlineUsesMonotonicTimeAndResetClearsExpiredOwnership()
|
|
{
|
|
var time = new ManualTimeProvider(DateTimeOffset.UnixEpoch);
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine, timeProvider: time);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x7000104Bu, 1);
|
|
_ = AttachBody(lifetime, record, SourceCell);
|
|
RuntimeSetPositionOutcome deferred = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(CrossLandblockRequest()));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
deferred.Projection));
|
|
|
|
time.JumpUtc(TimeSpan.FromDays(30));
|
|
lifetime.Physics.SetPosition.TickLostCellDeadlines();
|
|
Assert.Equal(0, lifetime.Physics.CaptureOwnership()
|
|
.ExpiredLostCellCount);
|
|
time.Advance(TimeSpan.FromSeconds(26));
|
|
lifetime.Physics.SetPosition.TickLostCellDeadlines();
|
|
Assert.Equal(1, lifetime.Physics.CaptureOwnership()
|
|
.ExpiredLostCellCount);
|
|
|
|
_ = lifetime.BeginSessionClear();
|
|
Assert.Equal(0, lifetime.Physics.CaptureOwnership()
|
|
.ExpiredLostCellCount);
|
|
Assert.False(lifetime.Physics.SetPosition.TryDequeueExpiredLostCell(out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void LostDeadlineTickIsZeroAllocationWhenEmptyAndExpiresByDeadlinePriority()
|
|
{
|
|
var time = new ManualTimeProvider(DateTimeOffset.UnixEpoch);
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(
|
|
engine,
|
|
timeProvider: time);
|
|
lifetime.Physics.SetPosition.TickLostCellDeadlines();
|
|
long before = GC.GetAllocatedBytesForCurrentThread();
|
|
for (int index = 0; index < 1_000; index++)
|
|
lifetime.Physics.SetPosition.TickLostCellDeadlines();
|
|
Assert.Equal(0, GC.GetAllocatedBytesForCurrentThread() - before);
|
|
|
|
RuntimeEntityRecord first = CreateRecord(lifetime, 0x70001101u, 1);
|
|
RuntimeEntityRecord middle = CreateRecord(lifetime, 0x70001102u, 1);
|
|
RuntimeEntityRecord last = CreateRecord(lifetime, 0x70001103u, 1);
|
|
foreach (RuntimeEntityRecord record in new[] { first, middle, last })
|
|
{
|
|
_ = AttachBody(lifetime, record, SourceCell);
|
|
RuntimeSetPositionOutcome deferred = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(CrossLandblockRequest()));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
deferred.Projection));
|
|
time.Advance(TimeSpan.FromSeconds(1));
|
|
}
|
|
lifetime.Physics.SetPosition.LeaveWorld(middle);
|
|
time.Advance(TimeSpan.FromSeconds(24));
|
|
lifetime.Physics.SetPosition.TickLostCellDeadlines();
|
|
|
|
Assert.True(lifetime.Physics.SetPosition.TryDequeueExpiredLostCell(
|
|
out RuntimeEntityKey firstExpired));
|
|
Assert.True(lifetime.Physics.SetPosition.TryDequeueExpiredLostCell(
|
|
out RuntimeEntityKey secondExpired));
|
|
Assert.Equal(first.Key, firstExpired);
|
|
Assert.Equal(last.Key, secondExpired);
|
|
Assert.False(lifetime.Physics.SetPosition.TryDequeueExpiredLostCell(out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void RearmedLostDeadlineSupersedesItsStalePriorityEntry()
|
|
{
|
|
var time = new ManualTimeProvider(DateTimeOffset.UnixEpoch);
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(
|
|
engine,
|
|
timeProvider: time);
|
|
RuntimeEntityRecord first = CreateRecord(lifetime, 0x70001112u, 1);
|
|
RuntimeEntityRecord second = CreateRecord(lifetime, 0x70001113u, 1);
|
|
_ = AttachBody(lifetime, first, SourceCell);
|
|
_ = AttachBody(lifetime, second, SourceCell);
|
|
|
|
RuntimeSetPositionOutcome firstLost = lifetime.Physics.SetPosition.Apply(
|
|
first,
|
|
first.PositionAuthorityVersion,
|
|
Command(CrossLandblockRequest()));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
firstLost.Projection));
|
|
time.Advance(TimeSpan.FromSeconds(1));
|
|
RuntimeSetPositionOutcome secondLost = lifetime.Physics.SetPosition.Apply(
|
|
second,
|
|
second.PositionAuthorityVersion,
|
|
Command(CrossLandblockRequest()));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
secondLost.Projection));
|
|
time.Advance(TimeSpan.FromSeconds(1));
|
|
|
|
RuntimeEntityPlacementToken replacement = lifetime.Physics.SetPosition
|
|
.BeginAcceptedPlacement(
|
|
first,
|
|
first.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
RuntimeSetPositionOutcome rearmed = lifetime.Physics.SetPosition
|
|
.SubmitPreparedPlacement(replacement, Command(CrossLandblockRequest()));
|
|
Assert.Equal(RuntimeSetPositionStatus.DeferredCell, rearmed.Status);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
rearmed.Projection));
|
|
|
|
time.Advance(TimeSpan.FromSeconds(24.5));
|
|
lifetime.Physics.SetPosition.TickLostCellDeadlines();
|
|
Assert.True(lifetime.Physics.SetPosition.TryDequeueExpiredLostCell(
|
|
out RuntimeEntityKey expired));
|
|
Assert.Equal(second.Key, expired);
|
|
Assert.False(lifetime.Physics.SetPosition.TryDequeueExpiredLostCell(out _));
|
|
time.Advance(TimeSpan.FromSeconds(1));
|
|
lifetime.Physics.SetPosition.TickLostCellDeadlines();
|
|
Assert.True(lifetime.Physics.SetPosition.TryDequeueExpiredLostCell(
|
|
out expired));
|
|
Assert.Equal(first.Key, expired);
|
|
}
|
|
|
|
[Fact]
|
|
public void RepeatedLostDeadlineRearmAndCancelKeepsExactHeapBounded()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001119u, 1);
|
|
_ = AttachBody(lifetime, record, SourceCell);
|
|
RuntimeSetPositionOutcome deferred = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(CrossLandblockRequest()));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
deferred.Projection));
|
|
|
|
for (int iteration = 0; iteration < 100; iteration++)
|
|
{
|
|
RuntimeEntityPlacementToken token = lifetime.Physics.SetPosition
|
|
.BeginAcceptedPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
deferred = lifetime.Physics.SetPosition.SubmitPreparedPlacement(
|
|
token,
|
|
Command(CrossLandblockRequest()));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
deferred.Projection));
|
|
RuntimeSetPositionOwnershipSnapshot snapshot = lifetime.Physics
|
|
.SetPosition.CaptureOwnership();
|
|
Assert.True(snapshot.IndexesConsistent);
|
|
Assert.Equal(1, snapshot.LostDeadlineCount);
|
|
Assert.Equal(1, snapshot.LostDeadlineNodeCount);
|
|
Assert.Equal(1, snapshot.LostDeadlineIndexCount);
|
|
}
|
|
|
|
lifetime.Physics.SetPosition.LeaveWorld(record);
|
|
RuntimeSetPositionOwnershipSnapshot final = lifetime.Physics
|
|
.SetPosition.CaptureOwnership();
|
|
Assert.True(final.IndexesConsistent);
|
|
Assert.Equal(0, final.LostDeadlineCount);
|
|
Assert.Equal(0, final.LostDeadlineNodeCount);
|
|
Assert.Equal(0, final.LostDeadlineIndexCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void CommittedGenerationWithoutExactIndoorCellRebindsAndWakesOnNextGeneration()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001114u, 1);
|
|
_ = AttachBody(lifetime, record, SourceCell);
|
|
var observer = new PlacementObserver();
|
|
using IDisposable subscription = lifetime.Events.SubscribePlacement(observer);
|
|
RuntimeSetPositionOutcome deferred = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(Request(
|
|
DestinationIndoorCell,
|
|
new Vector3(10f, 12f, 7f))));
|
|
Assert.Equal(RuntimeSetPositionStatus.DeferredCell, deferred.Status);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
deferred.Projection));
|
|
|
|
RuntimeCollisionAdmission missing = lifetime.Physics
|
|
.BeginCollisionAdmission(DestinationLandblock);
|
|
using (PreparedLandblockCollisionGeneration prepared =
|
|
lifetime.Physics.PrepareCollisionGeneration(missing))
|
|
{
|
|
lifetime.Physics.StageCollisionAssets(
|
|
missing,
|
|
prepared,
|
|
CollisionAssets(DestinationLandblock));
|
|
Assert.True(CommitPrepared(
|
|
lifetime.Physics,
|
|
missing,
|
|
prepared).Committed);
|
|
}
|
|
Assert.Single(observer.Deltas);
|
|
RuntimePhysicsOwnershipSnapshot unbound = lifetime.Physics
|
|
.CaptureOwnership();
|
|
Assert.Equal(1, unbound.UnboundDeferredSetPositionCellCount);
|
|
Assert.Equal(1, unbound.UnboundDeferredSetPositionCellOrderCount);
|
|
|
|
RuntimeCollisionAdmission resident = lifetime.Physics
|
|
.BeginCollisionAdmission(DestinationLandblock);
|
|
using PreparedLandblockCollisionGeneration preparedResident =
|
|
lifetime.Physics.PrepareCollisionGeneration(resident);
|
|
lifetime.Physics.StageCollisionAssets(
|
|
resident,
|
|
preparedResident,
|
|
CollisionAssets(DestinationLandblock));
|
|
AddSyntheticCell(preparedResident.DataCache, DestinationIndoorCell);
|
|
Assert.True(CommitPrepared(
|
|
lifetime.Physics,
|
|
resident,
|
|
preparedResident).Committed);
|
|
|
|
RuntimePlacementProjectionSnapshot placed = observer.Deltas[^1].Placement;
|
|
Assert.Equal(RuntimePlacementProjectionKind.Place, placed.Kind);
|
|
Assert.Equal(DestinationIndoorCell, placed.Token.ExactCellId);
|
|
RuntimePhysicsOwnershipSnapshot final = lifetime.Physics.CaptureOwnership();
|
|
Assert.Equal(0, final.DeferredSetPositionBucketCount);
|
|
Assert.Equal(0, final.UnboundDeferredSetPositionCellCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void CollisionAdmissionSupersessionAndInvalidationPreserveDeferredSurvivorOrder()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord first = CreateRecord(lifetime, 0x70001115u, 1);
|
|
RuntimeEntityRecord second = CreateRecord(lifetime, 0x70001116u, 1);
|
|
_ = AttachBody(lifetime, first, SourceCell);
|
|
_ = AttachBody(lifetime, second, SourceCell);
|
|
var observer = new PlacementObserver();
|
|
using IDisposable subscription = lifetime.Events.SubscribePlacement(observer);
|
|
foreach (RuntimeEntityRecord record in new[] { first, second })
|
|
{
|
|
RuntimeSetPositionOutcome deferred = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(Request(
|
|
DestinationIndoorCell,
|
|
new Vector3(10f, 12f, 7f))));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
deferred.Projection));
|
|
}
|
|
observer.Deltas.Clear();
|
|
|
|
RuntimeCollisionAdmission superseded = lifetime.Physics
|
|
.BeginCollisionAdmission(DestinationLandblock);
|
|
RuntimeCollisionAdmission invalidated = lifetime.Physics
|
|
.BeginCollisionAdmission(DestinationLandblock);
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
lifetime.Physics.PrepareCollisionGeneration(superseded));
|
|
lifetime.Physics.CancelCollisionGeneration(invalidated);
|
|
RuntimeCollisionAdmission current = lifetime.Physics
|
|
.BeginCollisionAdmission(DestinationLandblock);
|
|
using PreparedLandblockCollisionGeneration prepared = lifetime.Physics
|
|
.PrepareCollisionGeneration(current);
|
|
lifetime.Physics.StageCollisionAssets(
|
|
current,
|
|
prepared,
|
|
CollisionAssets(DestinationLandblock));
|
|
AddSyntheticCell(prepared.DataCache, DestinationIndoorCell);
|
|
Assert.True(CommitPrepared(
|
|
lifetime.Physics,
|
|
current,
|
|
prepared).Committed);
|
|
|
|
Assert.Equal(2, observer.Deltas.Count);
|
|
Assert.Equal(first.Key, observer.Deltas[0].Placement.Token.Entity);
|
|
Assert.Equal(second.Key, observer.Deltas[1].Placement.Token.Entity);
|
|
Assert.All(observer.Deltas, delta => Assert.Equal(
|
|
RuntimePlacementProjectionKind.Place,
|
|
delta.Placement.Kind));
|
|
Assert.True(lifetime.Physics.CaptureOwnership()
|
|
.DeferredSetPositionBucketCount == 0);
|
|
}
|
|
|
|
[Fact]
|
|
public void NextAdmissionMergesOlderUnboundAndNewerFutureBoundSurvivors()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord older = CreateRecord(lifetime, 0x7000111Fu, 1);
|
|
RuntimeEntityRecord newer = CreateRecord(lifetime, 0x70001120u, 1);
|
|
_ = AttachBody(lifetime, older, SourceCell);
|
|
_ = AttachBody(lifetime, newer, SourceCell);
|
|
var observer = new PlacementObserver();
|
|
using IDisposable subscription = lifetime.Events.SubscribePlacement(observer);
|
|
|
|
RuntimeSetPositionOutcome olderLost = lifetime.Physics.SetPosition.Apply(
|
|
older,
|
|
older.PositionAuthorityVersion,
|
|
Command(Request(
|
|
DestinationIndoorCell,
|
|
new Vector3(10f, 12f, 7f))));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
olderLost.Projection));
|
|
RuntimeCollisionAdmission cancelled = lifetime.Physics
|
|
.BeginCollisionAdmission(DestinationLandblock);
|
|
lifetime.Physics.CancelCollisionGeneration(cancelled);
|
|
|
|
RuntimeSetPositionOutcome newerLost = lifetime.Physics.SetPosition.Apply(
|
|
newer,
|
|
newer.PositionAuthorityVersion,
|
|
Command(Request(
|
|
DestinationIndoorCell,
|
|
new Vector3(11f, 12f, 7f))));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
newerLost.Projection));
|
|
RuntimePhysicsOwnershipSnapshot split = lifetime.Physics.CaptureOwnership();
|
|
Assert.Equal(1, split.UnboundDeferredSetPositionCellCount);
|
|
Assert.Equal(1, split.DeferredSetPositionBucketCount);
|
|
observer.Deltas.Clear();
|
|
|
|
RuntimeCollisionAdmission current = lifetime.Physics
|
|
.BeginCollisionAdmission(DestinationLandblock);
|
|
RuntimePhysicsOwnershipSnapshot merged = lifetime.Physics.CaptureOwnership();
|
|
Assert.Equal(0, merged.UnboundDeferredSetPositionCellCount);
|
|
Assert.Equal(1, merged.DeferredSetPositionBucketCount);
|
|
using PreparedLandblockCollisionGeneration prepared = lifetime.Physics
|
|
.PrepareCollisionGeneration(current);
|
|
lifetime.Physics.StageCollisionAssets(
|
|
current,
|
|
prepared,
|
|
CollisionAssets(DestinationLandblock));
|
|
AddSyntheticCell(prepared.DataCache, DestinationIndoorCell);
|
|
Assert.True(CommitPrepared(
|
|
lifetime.Physics,
|
|
current,
|
|
prepared).Committed);
|
|
|
|
Assert.Equal(2, observer.Deltas.Count);
|
|
Assert.Equal(older.Key, observer.Deltas[0].Placement.Token.Entity);
|
|
Assert.Equal(newer.Key, observer.Deltas[1].Placement.Token.Entity);
|
|
Assert.All(observer.Deltas, delta => Assert.Equal(
|
|
RuntimePlacementProjectionKind.Place,
|
|
delta.Placement.Kind));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false, 0x0101u)]
|
|
[InlineData(true, 0x0001u)]
|
|
public void CollisionRetirementParksOnlyAffectedDynamicParentlessRoots(
|
|
bool fullWithdrawal,
|
|
uint lowCell)
|
|
{
|
|
const uint landblock = 0x01010000u;
|
|
PhysicsEngine engine = FlatEngine(landblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord dynamicRoot = CreateRecord(lifetime, 0x70001007u, 1);
|
|
uint cell = landblock | lowCell;
|
|
PhysicsBody dynamicBody = AttachBody(
|
|
lifetime,
|
|
dynamicRoot,
|
|
landblock | 0x0001u);
|
|
ImmutableArray<FlatCollisionSphere> authoredSpheres =
|
|
[
|
|
new FlatCollisionSphere(new Vector3(0.25f, 0f, 0f), 0.3f),
|
|
new FlatCollisionSphere(new Vector3(-0.2f, 0.1f, 0.4f), 0.2f),
|
|
];
|
|
RuntimeSetPositionOutcome prepared = lifetime.Physics.SetPosition.Apply(
|
|
dynamicRoot,
|
|
dynamicRoot.PositionAuthorityVersion,
|
|
Command(Request(
|
|
landblock | 0x0001u,
|
|
new Vector3(12f, 18f, 7f)) with
|
|
{
|
|
Spheres = authoredSpheres,
|
|
}));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
prepared.Projection));
|
|
Assert.True(lifetime.Physics.SetPosition.TryGetPreparedMoverSphereCount(
|
|
dynamicRoot,
|
|
out int sphereCount));
|
|
Assert.Equal(2, sphereCount);
|
|
lifetime.Entities.SetFullCell(
|
|
dynamicRoot,
|
|
cell,
|
|
landblock | 0xFFFFu);
|
|
dynamicBody.SnapToCell(cell, dynamicBody.Position, dynamicBody.Position);
|
|
RuntimeEntityRecord staticRoot = CreateRecord(lifetime, 0x70001008u, 1);
|
|
_ = AttachBody(lifetime, staticRoot, cell, PhysicsStateFlags.Static);
|
|
RuntimeEntityRecord parent = CreateRecord(lifetime, 0x7000104Cu, 1);
|
|
_ = AttachBody(lifetime, parent, SourceCell);
|
|
RuntimeEntityRecord attached = CreateRecord(lifetime, 0x7000104Du, 1);
|
|
_ = AttachBody(lifetime, attached, cell);
|
|
CommitParent(lifetime, parent, attached);
|
|
|
|
lifetime.Physics.SetPosition.ParkCollisionResidents(
|
|
landblock,
|
|
includeOutdoorCells: fullWithdrawal);
|
|
|
|
Assert.True(lifetime.Physics.SetPosition.IsDeferred(dynamicRoot));
|
|
Assert.Equal(0, lifetime.Physics.CaptureOwnership()
|
|
.AwaitingSetPositionPreparationCount);
|
|
Assert.Equal(0u, dynamicRoot.FullCellId);
|
|
Assert.False(lifetime.Physics.IsSpatialRoot(dynamicRoot));
|
|
Assert.False(lifetime.Physics.SetPosition.IsDeferred(staticRoot));
|
|
Assert.Equal(cell, staticRoot.FullCellId);
|
|
Assert.True(lifetime.Physics.IsSpatialRoot(staticRoot));
|
|
Assert.False(lifetime.Physics.SetPosition.IsDeferred(attached));
|
|
Assert.Equal(cell, attached.FullCellId);
|
|
|
|
lifetime.Entities.ParentAttachments.EndChildProjection(
|
|
attached.ServerGuid);
|
|
lifetime.Physics.SetPosition.ParkCollisionResidents(
|
|
landblock,
|
|
includeOutdoorCells: fullWithdrawal);
|
|
Assert.True(lifetime.Physics.SetPosition.IsDeferred(attached));
|
|
}
|
|
|
|
[Fact]
|
|
public void PickupStyleLeaveWorldCancelsLostWakeAndTerminalDisposeConverges()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001009u, 1);
|
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
|
|
RuntimeSetPositionOutcome deferred = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(CrossLandblockRequest()));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
deferred.Projection));
|
|
|
|
lifetime.Physics.SetPosition.LeaveWorld(record);
|
|
AddFlatLandblock(engine, DestinationLandblock, 192f);
|
|
lifetime.Physics.SetPosition.CommitCollisionGeneration(
|
|
DestinationLandblock,
|
|
generation: 1,
|
|
ready: true);
|
|
|
|
Assert.Equal(0u, record.FullCellId);
|
|
Assert.Equal(0u, body.CellPosition.ObjCellId);
|
|
Assert.False(body.InWorld);
|
|
Assert.False(lifetime.Physics.SetPosition.IsDeferred(record));
|
|
|
|
lifetime.Dispose();
|
|
Assert.True(lifetime.Physics.CaptureOwnership().IsConverged);
|
|
}
|
|
|
|
/// <summary>
|
|
/// F2 regression: the C2 Operation pool retains full previous-generation
|
|
/// entity graphs (Record -> Snapshot/PhysicsBody/clock/host references)
|
|
/// through every pooled instance until it is rented and reset - a
|
|
/// session reset must not leave that behind uncleared, even though
|
|
/// PooledOperationCount is deliberately excluded from
|
|
/// <see cref="RuntimeSetPositionOwnershipSnapshot.IsConverged"/> (see
|
|
/// its doc comment).
|
|
/// </summary>
|
|
[Fact]
|
|
public void OperationPoolClearsOnResetSession()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001048u, 1);
|
|
_ = AttachBody(lifetime, record, SourceCell);
|
|
|
|
RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(Request(SourceCell, new Vector3(11f, 18f, 7f))));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
outcome.Projection));
|
|
|
|
RuntimeSetPositionOwnershipSnapshot beforeReset =
|
|
lifetime.Physics.SetPosition.CaptureOwnership();
|
|
Assert.True(beforeReset.PooledOperationCount >= 1);
|
|
|
|
lifetime.Physics.SetPosition.ResetSession();
|
|
|
|
Assert.Equal(
|
|
0,
|
|
lifetime.Physics.SetPosition.CaptureOwnership().PooledOperationCount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// F2 regression: same guarantee as
|
|
/// <see cref="OperationPoolClearsOnResetSession"/>, via Dispose instead
|
|
/// of ResetSession - both callers of ClearOwnedState must clear the
|
|
/// pool.
|
|
/// </summary>
|
|
[Fact]
|
|
public void OperationPoolClearsOnDispose()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001049u, 1);
|
|
_ = AttachBody(lifetime, record, SourceCell);
|
|
|
|
RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(Request(SourceCell, new Vector3(11f, 18f, 7f))));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
outcome.Projection));
|
|
|
|
RuntimeSetPositionOwnershipSnapshot beforeDispose =
|
|
lifetime.Physics.SetPosition.CaptureOwnership();
|
|
Assert.True(beforeDispose.PooledOperationCount >= 1);
|
|
|
|
lifetime.Dispose();
|
|
|
|
Assert.Equal(
|
|
0,
|
|
lifetime.Physics.SetPosition.CaptureOwnership().PooledOperationCount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// F4 regression: converting Operation's properties from
|
|
/// <c>required { get; init; }</c> to plain <c>{ get; set; }</c> (so
|
|
/// pooling could recycle instances) dropped the compiler's completeness
|
|
/// net - nothing any longer forces every construction site to set every
|
|
/// field. This reflection-based test is the runtime replacement: it
|
|
/// pins the exact set of backing fields Operation declares (private, so
|
|
/// only reflection can reach it from a test) against a hardcoded,
|
|
/// maintained list. Adding a new auto-property to Operation changes its
|
|
/// backing-field set and fails this test immediately - the failure
|
|
/// message is the prompt to update BOTH this list and
|
|
/// <c>Operation.ResetAllFieldsToDefault</c> in the same change, exactly
|
|
/// mirroring what a missing `required` member assignment used to force
|
|
/// at compile time.
|
|
/// </summary>
|
|
[Fact]
|
|
public void OperationResetAllFieldsToDefaultTouchesEveryDeclaredField()
|
|
{
|
|
Type? operationType = typeof(RuntimeSetPositionState).GetNestedType(
|
|
"Operation",
|
|
BindingFlags.NonPublic);
|
|
Assert.NotNull(operationType);
|
|
|
|
FieldInfo[] actualFields = operationType!.GetFields(
|
|
BindingFlags.Instance
|
|
| BindingFlags.NonPublic
|
|
| BindingFlags.Public);
|
|
string[] actualFieldNames = actualFields
|
|
.Select(field => field.Name)
|
|
.OrderBy(name => name, StringComparer.Ordinal)
|
|
.ToArray();
|
|
|
|
// The maintained list: one entry per auto-property Operation
|
|
// declares. Kept as property names (not the `<Name>k__BackingField`
|
|
// form the compiler actually emits) so a reviewer can read it
|
|
// directly against the property list in the source file.
|
|
string[] expectedPropertyNames =
|
|
[
|
|
"Record",
|
|
"Body",
|
|
"Token",
|
|
"Key",
|
|
"PositionAuthorityVersion",
|
|
"SessionLifetimeVersion",
|
|
"SourceSpatialAuthorityVersion",
|
|
"SourceVelocityAuthorityVersion",
|
|
"PreviousContact",
|
|
"PreviousOnWalkable",
|
|
"Command",
|
|
"Result",
|
|
"SpatialAuthorityVersion",
|
|
"PlacementCommitVersion",
|
|
"ExactCellId",
|
|
"CollisionGeneration",
|
|
"CollisionPrefix",
|
|
"WithdrawalAcknowledged",
|
|
"CollisionGenerationReady",
|
|
"CollisionQuiescenceHeld",
|
|
"ProjectionSequence",
|
|
"WakeableLostCell",
|
|
"Stage",
|
|
"Kind",
|
|
"Portal",
|
|
"RequiresPreparation",
|
|
"Expired",
|
|
"LostFamilyKeys",
|
|
"InheritedLostDeadline",
|
|
"EnteringWorldFromCelllessResidence",
|
|
"DormantLocalActivation",
|
|
"ParkReason",
|
|
"PreparedCommandAwaitingWithdrawalAck",
|
|
"ParkWithdrawal",
|
|
"InPool",
|
|
];
|
|
string[] expectedFieldNames = expectedPropertyNames
|
|
.Select(name => $"<{name}>k__BackingField")
|
|
.OrderBy(name => name, StringComparer.Ordinal)
|
|
.ToArray();
|
|
|
|
Assert.Equal(expectedFieldNames, actualFieldNames);
|
|
|
|
MethodInfo? resetMethod = operationType.GetMethod(
|
|
"ResetAllFieldsToDefault",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
Assert.NotNull(resetMethod);
|
|
}
|
|
|
|
[Fact]
|
|
public void CommittedParentDoesNotLeakAcrossChildGuidReuse()
|
|
{
|
|
const uint childGuid = 0x7000104Eu;
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord parent = CreateRecord(lifetime, 0x7000104Fu, 1);
|
|
RuntimeEntityRecord retired = CreateRecord(lifetime, childGuid, 1);
|
|
_ = AttachBody(lifetime, parent, SourceCell);
|
|
_ = AttachBody(lifetime, retired, SourceCell);
|
|
CommitParent(lifetime, parent, retired);
|
|
Assert.True(lifetime.TryAcceptDelete(
|
|
new DeleteObject.Parsed(childGuid, 1),
|
|
isLocalPlayer: false,
|
|
removeRetainedObject: false,
|
|
out RuntimeEntityDeleteAcceptance acceptance));
|
|
lifetime.CompleteAcceptedDelete(acceptance);
|
|
Assert.Null(lifetime.RetireCanonicalOnly(retired));
|
|
|
|
RuntimeEntityRecord replacement = CreateRecord(lifetime, childGuid, 2);
|
|
_ = AttachBody(lifetime, replacement, SourceCell);
|
|
lifetime.Physics.SetPosition.ParkCollisionResidents(
|
|
SourceLandblock,
|
|
includeOutdoorCells: true);
|
|
|
|
Assert.True(lifetime.Physics.SetPosition.IsDeferred(replacement));
|
|
}
|
|
|
|
[Fact]
|
|
public void NewerPositionPickupAndParentEachCancelExactLostOperation()
|
|
{
|
|
VerifyPositionChannelCancellation(CancellationChannel.Position);
|
|
VerifyPositionChannelCancellation(CancellationChannel.Pickup);
|
|
VerifyPositionChannelCancellation(CancellationChannel.Parent);
|
|
}
|
|
|
|
[Fact]
|
|
public void DeleteGuidReuseAndSessionResetCannotWakeStaleIncarnation()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
const uint guid = 0x7000100Du;
|
|
RuntimeEntityRecord retired = CreateRecord(lifetime, guid, 1);
|
|
_ = AttachBody(lifetime, retired, SourceCell);
|
|
RuntimeSetPositionOutcome deferred = lifetime.Physics.SetPosition.Apply(
|
|
retired,
|
|
retired.PositionAuthorityVersion,
|
|
Command(CrossLandblockRequest()));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
deferred.Projection));
|
|
Assert.True(lifetime.TryAcceptDelete(
|
|
new DeleteObject.Parsed(guid, 1),
|
|
isLocalPlayer: false,
|
|
removeRetainedObject: false,
|
|
out RuntimeEntityDeleteAcceptance acceptance));
|
|
lifetime.CompleteAcceptedDelete(acceptance);
|
|
Assert.Null(lifetime.RetireCanonicalOnly(retired));
|
|
|
|
RuntimeEntityRecord replacement = CreateRecord(lifetime, guid, 2);
|
|
PhysicsBody replacementBody = AttachBody(
|
|
lifetime,
|
|
replacement,
|
|
SourceCell);
|
|
AddFlatLandblock(engine, DestinationLandblock, 192f);
|
|
lifetime.Physics.SetPosition.CommitCollisionGeneration(
|
|
DestinationLandblock,
|
|
generation: 1,
|
|
ready: true);
|
|
Assert.Equal(SourceCell, replacement.FullCellId);
|
|
Assert.Equal(SourceCell, replacementBody.CellPosition.ObjCellId);
|
|
Assert.False(lifetime.Physics.SetPosition.IsDeferred(replacement));
|
|
|
|
RuntimeSetPositionOutcome pending = lifetime.Physics.SetPosition.Apply(
|
|
replacement,
|
|
replacement.PositionAuthorityVersion,
|
|
Command(Request(SourceCell, new Vector3(15f, 18f, 7f))));
|
|
Assert.True(pending.Projection.IsValid);
|
|
IReadOnlyList<RuntimeEntityRecord> retiring = lifetime.BeginSessionClear();
|
|
Assert.Single(retiring);
|
|
Assert.Equal(0, lifetime.Physics.CaptureOwnership()
|
|
.SetPositionOperationCount);
|
|
Assert.Equal(0, lifetime.Physics.CaptureOwnership()
|
|
.PendingSetPositionHostAcknowledgementCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void ColdCollisionRetirementWaitsForExactAuthoredMoverPreparation()
|
|
{
|
|
const uint landblock = 0x01010000u;
|
|
PhysicsEngine engine = FlatEngine(landblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x7000100Eu, 1);
|
|
_ = AttachBody(lifetime, record, landblock | 0x0101u);
|
|
|
|
lifetime.Physics.SetPosition.ParkCollisionResidents(
|
|
landblock,
|
|
includeOutdoorCells: false);
|
|
|
|
Assert.True(lifetime.Physics.SetPosition.IsDeferred(record));
|
|
Assert.Equal(1, lifetime.Physics.CaptureOwnership()
|
|
.AwaitingSetPositionPreparationCount);
|
|
Assert.True(lifetime.Physics.SetPosition.TryGetAwaitingPreparationToken(
|
|
record,
|
|
out RuntimeEntityPlacementToken token));
|
|
Assert.True(lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot withdrawal));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
withdrawal.Token));
|
|
ImmutableArray<FlatCollisionSphere> authored =
|
|
[
|
|
new FlatCollisionSphere(new Vector3(0.4f, 0f, 0f), 0.35f),
|
|
new FlatCollisionSphere(new Vector3(-0.3f, 0f, 0.5f), 0.2f),
|
|
];
|
|
RuntimeSetPositionCommand preparedCommand = PrepareAuthoredCommand(
|
|
lifetime,
|
|
token,
|
|
authored);
|
|
RuntimeSetPositionOutcome supplied = lifetime.Physics.SetPosition
|
|
.SubmitPreparedPlacement(token, preparedCommand);
|
|
|
|
Assert.Equal(RuntimeSetPositionStatus.DeferredCell, supplied.Status);
|
|
Assert.Equal(0, lifetime.Physics.CaptureOwnership()
|
|
.AwaitingSetPositionPreparationCount);
|
|
Assert.False(lifetime.Physics.SetPosition.TryGetPreparedMoverSphereCount(
|
|
record,
|
|
out _));
|
|
AddSyntheticCell(engine.DataCache!, landblock | 0x0101u);
|
|
lifetime.Physics.SetPosition.CommitCollisionGeneration(
|
|
landblock,
|
|
generation: 1,
|
|
ready: true);
|
|
Assert.True(lifetime.Physics.SetPosition.TryGetPreparedMoverSphereCount(
|
|
record,
|
|
out int sphereCount));
|
|
Assert.Equal(2, sphereCount);
|
|
Assert.Equal(landblock | 0x0101u,
|
|
record.PhysicsBody!.CellPosition.ObjCellId);
|
|
Assert.True(lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot placed));
|
|
Assert.Equal(RuntimePlacementProjectionKind.Place, placed.Kind);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
placed.Token));
|
|
}
|
|
|
|
[Fact]
|
|
public void ColdMalformedWakeReindexesUntilCorrectedPreparationAndNextAdmission()
|
|
{
|
|
const uint landblock = 0x01010000u;
|
|
uint exactCell = landblock | 0x0101u;
|
|
PhysicsEngine engine = FlatEngine(landblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001127u, 1);
|
|
_ = AttachBody(lifetime, record, exactCell);
|
|
var observer = new PlacementObserver();
|
|
using IDisposable subscription = lifetime.Events.SubscribePlacement(observer);
|
|
lifetime.Physics.SetPosition.ParkCollisionResidents(
|
|
landblock,
|
|
includeOutdoorCells: false);
|
|
Assert.True(lifetime.Physics.SetPosition.TryGetAwaitingPreparationToken(
|
|
record,
|
|
out RuntimeEntityPlacementToken token));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
Assert.Single(observer.Deltas).Placement.Token));
|
|
observer.Deltas.Clear();
|
|
|
|
var malformedSetup = new FlatSetupCollision(
|
|
ImmutableArray<FlatCollisionCylinder>.Empty,
|
|
[new FlatCollisionSphere(
|
|
new Vector3(float.NaN, 0f, 0f),
|
|
0.4f)],
|
|
height: 0f,
|
|
radius: 0f,
|
|
stepUpHeight: 0.4f,
|
|
stepDownHeight: 0.4f);
|
|
Assert.Equal(
|
|
RuntimeSetPositionMoverPreparationStatus.InvalidData,
|
|
lifetime.Physics.SetPosition.PrepareMover(
|
|
token,
|
|
new RuntimeSetPositionMoverPreparation(
|
|
RuntimeSetPositionMoverSetup.Resolved(
|
|
0x02000001u,
|
|
malformedSetup),
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative,
|
|
GameTime: 10d,
|
|
PhysicsPlacementClass.Ordinary,
|
|
PhysicsSetPositionFlags.Placement
|
|
| PhysicsSetPositionFlags.Slide),
|
|
out _));
|
|
|
|
RuntimeCollisionAdmission first = lifetime.Physics
|
|
.BeginCollisionAdmission(landblock);
|
|
using (PreparedLandblockCollisionGeneration prepared = lifetime.Physics
|
|
.PrepareCollisionGeneration(first))
|
|
{
|
|
lifetime.Physics.StageCollisionAssets(
|
|
first,
|
|
prepared,
|
|
CollisionAssets(landblock));
|
|
AddSyntheticCell(prepared.DataCache, exactCell);
|
|
Assert.True(CommitPrepared(
|
|
lifetime.Physics,
|
|
first,
|
|
prepared).Committed);
|
|
}
|
|
RuntimeSetPositionOwnershipSnapshot invalidWake = lifetime.Physics
|
|
.SetPosition.CaptureOwnership();
|
|
Assert.Equal(1, invalidWake.AwaitingPreparationCount);
|
|
Assert.Equal(0, invalidWake.DeferredBucketCount);
|
|
Assert.Equal(1, invalidWake.LostDeadlineCount);
|
|
Assert.Empty(observer.Deltas);
|
|
Assert.False(lifetime.Physics.SetPosition.TryGetPreparedMoverSphereCount(
|
|
record,
|
|
out _));
|
|
|
|
RuntimeSetPositionCommand corrected = PrepareAuthoredCommand(
|
|
lifetime,
|
|
token,
|
|
[new FlatCollisionSphere(Vector3.Zero, 0.4f)]);
|
|
RuntimeSetPositionOutcome correctedPending = lifetime.Physics
|
|
.SetPosition.SubmitPreparedPlacement(token, corrected);
|
|
Assert.Equal(RuntimeSetPositionStatus.DeferredCell,
|
|
correctedPending.Status);
|
|
Assert.Equal(PhysicsResidenceDisposition.Committed,
|
|
correctedPending.Residence);
|
|
Assert.Equal(exactCell, correctedPending.ExactCellId);
|
|
|
|
RuntimePlacementProjectionSnapshot placed = Assert.Single(observer.Deltas)
|
|
.Placement;
|
|
Assert.Equal(RuntimePlacementProjectionKind.Place, placed.Kind);
|
|
Assert.True(lifetime.Physics.SetPosition.TryGetPreparedMoverSphereCount(
|
|
record,
|
|
out int sphereCount));
|
|
Assert.Equal(1, sphereCount);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
placed.Token));
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// C0-3: exact-Setup mover chain end-to-end
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void TryPrepareAndSubmitAuthoredPlacement_ChainsSetupReadThroughPrepareMoverToSubmit()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001101u, 1);
|
|
AttachBody(lifetime, record, SourceCell);
|
|
RuntimeEntityPlacementToken token = lifetime.Physics.SetPosition
|
|
.BeginAuthoredPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
Assert.True(token.IsValid);
|
|
|
|
// Spawn's own default SetupTableId (0x02000001u) - the exact
|
|
// CanonicalSetupTableId CapturePreparationAuthority already trusts;
|
|
// the chain must read THIS id, not a caller-supplied one.
|
|
ImmutableArray<FlatCollisionSphere> spheres =
|
|
[
|
|
new FlatCollisionSphere(new Vector3(0f, 0f, 0.5f), 0.4f),
|
|
new FlatCollisionSphere(new Vector3(0f, 0f, 1.2f), 0.4f),
|
|
];
|
|
var source = new FakeCollisionSource(
|
|
0x02000001u,
|
|
new FlatSetupCollision(
|
|
ImmutableArray<FlatCollisionCylinder>.Empty,
|
|
spheres,
|
|
height: 0f,
|
|
radius: 0f,
|
|
stepUpHeight: 0.4f,
|
|
stepDownHeight: 0.35f));
|
|
|
|
Assert.Equal(
|
|
RuntimeSetPositionMoverPreparationStatus.Prepared,
|
|
lifetime.Physics.SetPosition.TryPrepareAndSubmitAuthoredPlacement(
|
|
record,
|
|
token,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative,
|
|
PhysicsSetPositionFlags.Placement
|
|
| PhysicsSetPositionFlags.Slide,
|
|
source,
|
|
gameTime: 10d,
|
|
out RuntimeSetPositionOutcome outcome));
|
|
|
|
Assert.Equal(
|
|
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
|
|
outcome.Status);
|
|
Assert.Equal(1, source.ReadCount);
|
|
Assert.True(lifetime.Physics.SetPosition.TryGetPreparedMoverSphereCount(
|
|
record,
|
|
out int sphereCount));
|
|
Assert.Equal(2, sphereCount);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
outcome.Projection));
|
|
}
|
|
|
|
[Fact]
|
|
public void TryPrepareAndSubmitAuthoredPlacement_YieldsRetryOnAMissingSetupReadWithoutMutatingStage()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001102u, 1);
|
|
AttachBody(lifetime, record, SourceCell);
|
|
RuntimeEntityPlacementToken token = lifetime.Physics.SetPosition
|
|
.BeginAuthoredPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
Assert.True(token.IsValid);
|
|
|
|
var source = new FakeCollisionSource(
|
|
0x02000001u,
|
|
setup: null,
|
|
status: PreparedAssetReadStatus.Missing);
|
|
|
|
Assert.Equal(
|
|
RuntimeSetPositionMoverPreparationStatus.RetrySetupUnavailable,
|
|
lifetime.Physics.SetPosition.TryPrepareAndSubmitAuthoredPlacement(
|
|
record,
|
|
token,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative,
|
|
PhysicsSetPositionFlags.Placement
|
|
| PhysicsSetPositionFlags.Slide,
|
|
source,
|
|
gameTime: 10d,
|
|
out RuntimeSetPositionOutcome outcome));
|
|
|
|
Assert.Equal(default, outcome);
|
|
Assert.Equal(1, source.ReadCount);
|
|
// Never manufactured a fallback while the read is in flight - the
|
|
// token can still be prepared once the asset lands.
|
|
Assert.True(lifetime.Physics.SetPosition.IsPlacementCurrent(token));
|
|
Assert.False(lifetime.Physics.SetPosition.TryGetPreparedMoverSphereCount(
|
|
record,
|
|
out _));
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// C0-1: executor completion on the SAME ordered placement stream
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void PublishExecutorCompletion_PublishesAcknowledgeOnlyReceiptAndConverges()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001201u, 1);
|
|
PhysicsBody body = AttachBody(lifetime, record, SourceCell);
|
|
var observer = new PlacementObserver();
|
|
using IDisposable subscription =
|
|
lifetime.Events.SubscribePlacement(observer);
|
|
|
|
RuntimePlacementProjectionToken token =
|
|
lifetime.Physics.SetPosition.PublishExecutorCompletion(record);
|
|
|
|
Assert.True(token.IsValid);
|
|
Assert.Equal(record.Key, token.Entity);
|
|
RuntimePlacementProjectionSnapshot published =
|
|
Assert.Single(observer.Deltas).Placement;
|
|
Assert.Equal(RuntimePlacementProjectionKind.ExecutorCompleted,
|
|
published.Kind);
|
|
Assert.Equal(token, published.Token);
|
|
Assert.Equal(body.Position, published.WorldPosition);
|
|
Assert.Equal(1, lifetime.Physics.SetPosition.PendingProjectionCount);
|
|
|
|
// Acknowledge-only, exactly like Discard - no operation to resume.
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(token));
|
|
Assert.Equal(0, lifetime.Physics.SetPosition.PendingProjectionCount);
|
|
// A stale re-acknowledge of the already-consumed receipt fails.
|
|
Assert.False(lifetime.Physics.SetPosition.AcknowledgeProjection(token));
|
|
}
|
|
|
|
[Fact]
|
|
public void PublishExecutorCompletion_RespectsExactHeadOrderingAcrossEntities()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
RuntimeEntityRecord first = CreateRecord(lifetime, 0x70001202u, 1);
|
|
AttachBody(lifetime, first, SourceCell);
|
|
RuntimeEntityRecord second = CreateRecord(lifetime, 0x70001203u, 1);
|
|
AttachBody(lifetime, second, SourceCell);
|
|
|
|
RuntimePlacementProjectionToken firstToken =
|
|
lifetime.Physics.SetPosition.PublishExecutorCompletion(first);
|
|
RuntimePlacementProjectionToken secondToken =
|
|
lifetime.Physics.SetPosition.PublishExecutorCompletion(second);
|
|
|
|
Assert.Equal(2, lifetime.Physics.SetPosition.PendingProjectionCount);
|
|
// The exact head (first) must acknowledge before the second, matching
|
|
// the ordered-stream contract every other Kind already honors.
|
|
Assert.False(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
secondToken));
|
|
Assert.Equal(2, lifetime.Physics.SetPosition.PendingProjectionCount);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
firstToken));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
secondToken));
|
|
Assert.Equal(0, lifetime.Physics.SetPosition.PendingProjectionCount);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// C0-4: TryCommitParent/CommitWithdrawal cancellation asymmetries
|
|
// (the ordinary Physics.SetPosition.Forget half, isolated from any
|
|
// initial-create residence)
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void TryCommitParent_CancelsASeparateActiveOrdinaryPendingPlacement()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
const uint guid = 0x70001301u;
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, guid, 1);
|
|
AttachBody(lifetime, record, SourceCell);
|
|
RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(Request(SourceCell, new Vector3(12f, 18f, 7f))));
|
|
Assert.Equal(
|
|
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
|
|
outcome.Status);
|
|
Assert.Equal(1, lifetime.Physics.SetPosition.PendingProjectionCount);
|
|
|
|
var discards = new List<RuntimePlacementProjectionSnapshot>();
|
|
using IDisposable subscription = lifetime.Events.SubscribePlacement(
|
|
new PlacementObserver(delta =>
|
|
{
|
|
if (delta.Placement.Kind is RuntimePlacementProjectionKind.Discard)
|
|
discards.Add(delta.Placement);
|
|
}));
|
|
|
|
var relation = new ParentAttachmentRelation(
|
|
ParentGuid: 0x70001400u,
|
|
ChildGuid: guid,
|
|
ParentLocation: 1u,
|
|
PlacementId: 1u,
|
|
ParentInstanceSequence: 1,
|
|
ChildPositionSequence: 1);
|
|
Assert.True(lifetime.TryCommitParent(relation, null, out _));
|
|
|
|
Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership()
|
|
.ActiveOperationCount);
|
|
// The old Place receipt is REPLACED by a Discard at the same
|
|
// sequence, not removed outright - it still awaits host
|
|
// acknowledgement, exactly like every other cancelled-in-flight
|
|
// receipt.
|
|
Assert.Equal(1, lifetime.Physics.SetPosition.PendingProjectionCount);
|
|
RuntimePlacementProjectionSnapshot discard = Assert.Single(discards);
|
|
Assert.Equal(record.Key, discard.Token.Entity);
|
|
Assert.Equal(outcome.Projection.Sequence, discard.Token.Sequence);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
discard.Token));
|
|
Assert.Equal(0, lifetime.Physics.SetPosition.PendingProjectionCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void CommitWithdrawal_CancelsAnActiveOrdinaryPendingPlacementSymmetricallyWithPickup()
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
const uint guid = 0x70001302u;
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, guid, 1);
|
|
AttachBody(lifetime, record, SourceCell);
|
|
RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(Request(SourceCell, new Vector3(12f, 18f, 7f))));
|
|
Assert.Equal(
|
|
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
|
|
outcome.Status);
|
|
Assert.Equal(1, lifetime.Physics.SetPosition.PendingProjectionCount);
|
|
|
|
var discards = new List<RuntimePlacementProjectionSnapshot>();
|
|
using IDisposable subscription = lifetime.Events.SubscribePlacement(
|
|
new PlacementObserver(delta =>
|
|
{
|
|
if (delta.Placement.Kind is RuntimePlacementProjectionKind.Discard)
|
|
discards.Add(delta.Placement);
|
|
}));
|
|
|
|
Assert.True(lifetime.CommitWithdrawal(record));
|
|
|
|
Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership()
|
|
.ActiveOperationCount);
|
|
Assert.Equal(1, lifetime.Physics.SetPosition.PendingProjectionCount);
|
|
RuntimePlacementProjectionSnapshot discard = Assert.Single(discards);
|
|
Assert.Equal(record.Key, discard.Token.Entity);
|
|
Assert.Equal(outcome.Projection.Sequence, discard.Token.Sequence);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
discard.Token));
|
|
Assert.Equal(0, lifetime.Physics.SetPosition.PendingProjectionCount);
|
|
}
|
|
|
|
private static void VerifyPositionChannelCancellation(
|
|
CancellationChannel channel)
|
|
{
|
|
PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
|
|
using var lifetime = new RuntimeEntityObjectLifetime(engine);
|
|
uint guid = 0x7000100Au + (uint)channel;
|
|
RuntimeEntityRecord record = CreateRecord(lifetime, guid, 1);
|
|
_ = AttachBody(lifetime, record, SourceCell);
|
|
RuntimeSetPositionOutcome deferred = lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(CrossLandblockRequest()));
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
deferred.Projection));
|
|
|
|
switch (channel)
|
|
{
|
|
case CancellationChannel.Position:
|
|
Assert.True(lifetime.TryApplyPosition(
|
|
new WorldSession.EntityPositionUpdate(
|
|
guid,
|
|
new CreateObject.ServerPosition(
|
|
SourceCell, 11f, 20f, 7f, 1f, 0f, 0f, 0f),
|
|
Velocity: null,
|
|
PlacementId: null,
|
|
IsGrounded: true,
|
|
InstanceSequence: 1,
|
|
PositionSequence: 2,
|
|
TeleportSequence: 0,
|
|
ForcePositionSequence: 0),
|
|
isLocalPlayer: false,
|
|
forcePositionRotation: null,
|
|
currentLocalVelocity: null,
|
|
acknowledgeProjection: null,
|
|
out _,
|
|
out _,
|
|
out _));
|
|
break;
|
|
case CancellationChannel.Pickup:
|
|
Assert.True(lifetime.TryApplyPickup(
|
|
new PickupEvent.Parsed(guid, 1, 2),
|
|
acknowledgeProjection: null,
|
|
out _));
|
|
break;
|
|
case CancellationChannel.Parent:
|
|
RuntimeEntityRecord parent = CreateRecord(
|
|
lifetime,
|
|
guid + 0x100u,
|
|
4);
|
|
Assert.True(lifetime.TryApplyParent(
|
|
new ParentEvent.Parsed(
|
|
parent.ServerGuid,
|
|
guid,
|
|
ParentLocation: 1,
|
|
PlacementId: 2,
|
|
ParentInstanceSequence: parent.Incarnation,
|
|
ChildPositionSequence: 2),
|
|
acknowledgeProjection: null,
|
|
out _));
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException(nameof(channel));
|
|
}
|
|
|
|
Assert.False(lifetime.Physics.SetPosition.IsDeferred(record));
|
|
Assert.Equal(0, lifetime.Physics.CaptureOwnership()
|
|
.DeferredSetPositionCount);
|
|
Assert.Equal(0, lifetime.Physics.CaptureOwnership()
|
|
.LostCellDeadlineCount);
|
|
|
|
// Pickup and Parent are WITHDRAWAL transactions, not cancellations:
|
|
// each hand-rolls its own partial re-withdrawal after the Forget,
|
|
// undoing the object clock and FullCellId but NOTHING else. The
|
|
// park-rollback the accepted-Position merge opts into must never fire
|
|
// underneath them - it would set InWorld and re-add the entity to
|
|
// _spatialRoots, neither of which their cleanup undoes, leaving a
|
|
// picked-up item both in inventory AND an InWorld cell-less spatial
|
|
// root inside the CPhysics::UseTime workset. The counts above cannot
|
|
// see either field, so assert them directly.
|
|
//
|
|
// Position is the one CANCELLATION here: a newer accepted Position
|
|
// supersedes the placement while the entity stays in the world, so it
|
|
// must roll back rather than strand the body.
|
|
if (channel is CancellationChannel.Position)
|
|
{
|
|
Assert.True(
|
|
record.PhysicsBody?.InWorld ?? false,
|
|
"the cancellation channel must roll the park back");
|
|
Assert.True(
|
|
lifetime.Physics.IsSpatialRoot(record),
|
|
"the cancellation channel must restore the spatial root");
|
|
}
|
|
else
|
|
{
|
|
Assert.False(
|
|
record.PhysicsBody?.InWorld ?? false,
|
|
"a withdrawal channel must not leave the body InWorld");
|
|
Assert.False(
|
|
lifetime.Physics.IsSpatialRoot(record),
|
|
"a withdrawal channel must not leave the entity a spatial root");
|
|
}
|
|
}
|
|
|
|
private static void CommitParent(
|
|
RuntimeEntityObjectLifetime lifetime,
|
|
RuntimeEntityRecord parent,
|
|
RuntimeEntityRecord child)
|
|
{
|
|
var relation = new ParentAttachmentRelation(
|
|
parent.ServerGuid,
|
|
child.ServerGuid,
|
|
ParentLocation: 1,
|
|
PlacementId: 2,
|
|
ParentInstanceSequence: parent.Incarnation,
|
|
ChildPositionSequence: 1);
|
|
lifetime.Entities.ParentAttachments.AcceptCreateObjectRelation(relation);
|
|
Assert.True(lifetime.Entities.ParentAttachments.CommitProjection(relation));
|
|
}
|
|
|
|
private static RuntimeSetPositionCommand Command(
|
|
PhysicsSetPositionRequest request) => new(
|
|
request,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative,
|
|
GameTime: 10d,
|
|
ExpectedVelocityAuthorityVersion: 0UL,
|
|
ShadowWorldOffsetX: request.CellId == SourceCell ? 0f : 192f,
|
|
ShadowWorldOffsetY: 0f);
|
|
|
|
private static RuntimeSetPositionCommand PrepareAuthoredCommand(
|
|
RuntimeEntityObjectLifetime lifetime,
|
|
RuntimeEntityPlacementToken token,
|
|
ImmutableArray<FlatCollisionSphere> spheres)
|
|
{
|
|
var setup = new FlatSetupCollision(
|
|
ImmutableArray<FlatCollisionCylinder>.Empty,
|
|
spheres,
|
|
height: 0f,
|
|
radius: 0f,
|
|
stepUpHeight: 0.4f,
|
|
stepDownHeight: 0.4f);
|
|
var preparation = new RuntimeSetPositionMoverPreparation(
|
|
RuntimeSetPositionMoverSetup.Resolved(0x02000001u, setup),
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative,
|
|
GameTime: 10d,
|
|
PhysicsPlacementClass.Ordinary,
|
|
PhysicsSetPositionFlags.Placement
|
|
| PhysicsSetPositionFlags.Slide);
|
|
Assert.Equal(
|
|
RuntimeSetPositionMoverPreparationStatus.Prepared,
|
|
lifetime.Physics.SetPosition.PrepareMover(
|
|
token,
|
|
preparation,
|
|
out RuntimeSetPositionCommand command));
|
|
return command;
|
|
}
|
|
|
|
private static PhysicsSetPositionRequest CrossLandblockRequest() =>
|
|
Request(
|
|
SourceCell,
|
|
new Vector3(193f, 12f, 7f),
|
|
new Vector3(193f, 12f, 7f));
|
|
|
|
private static PhysicsSetPositionRequest Request(
|
|
uint cellId,
|
|
Vector3 position,
|
|
Vector3? cellLocal = null) => new(
|
|
position,
|
|
Quaternion.Identity,
|
|
cellId,
|
|
cellLocal ?? position,
|
|
ImmutableArray<FlatCollisionSphere>.Empty,
|
|
Scale: 1f,
|
|
StepUpHeight: 0.4f,
|
|
StepDownHeight: 0.4f,
|
|
Flags: PhysicsSetPositionFlags.Placement
|
|
| PhysicsSetPositionFlags.Slide);
|
|
|
|
private static RuntimeEntityRecord CreateRecord(
|
|
RuntimeEntityObjectLifetime lifetime,
|
|
uint guid,
|
|
ushort incarnation)
|
|
{
|
|
RuntimeEntityRecord record = lifetime.RegisterEntity(
|
|
Spawn(guid, incarnation)).Canonical!;
|
|
lifetime.Entities.SetFinalPhysicsState(record, PhysicsStateFlags.Gravity);
|
|
return record;
|
|
}
|
|
|
|
private static PhysicsBody AttachBody(
|
|
RuntimeEntityObjectLifetime lifetime,
|
|
RuntimeEntityRecord record,
|
|
uint cellId,
|
|
PhysicsStateFlags state = PhysicsStateFlags.Gravity)
|
|
{
|
|
lifetime.Entities.SetFullCell(
|
|
record,
|
|
cellId,
|
|
(cellId & 0xFFFF0000u) | 0xFFFFu);
|
|
lifetime.Entities.SetFinalPhysicsState(record, state);
|
|
var body = new PhysicsBody
|
|
{
|
|
Position = new Vector3(10f, 20f, 7f),
|
|
Orientation = Quaternion.Identity,
|
|
LastUpdateTime = 1d,
|
|
State = state,
|
|
TransientState = TransientStateFlags.Active,
|
|
};
|
|
body.SnapToCell(cellId, body.Position, body.Position);
|
|
lifetime.Entities.SetPhysicsBody(record, body);
|
|
record.ObjectClock.Activate();
|
|
lifetime.Physics.AcknowledgeSpatialProjection(record, spatial: true);
|
|
return body;
|
|
}
|
|
|
|
private static PhysicsEngine FlatEngine(uint landblock, float worldOffsetX)
|
|
{
|
|
var engine = new PhysicsEngine
|
|
{
|
|
DataCache = new PhysicsDataCache(),
|
|
};
|
|
AddFlatLandblock(engine, landblock, worldOffsetX);
|
|
return engine;
|
|
}
|
|
|
|
private static RuntimeLandblockCollisionAssets CollisionAssets(
|
|
uint landblockId) => new(
|
|
landblockId,
|
|
new TerrainSurface(new byte[81], new float[256]),
|
|
Array.Empty<CellSurface>(),
|
|
Array.Empty<PortalPlane>(),
|
|
WorldOffsetX: 192f,
|
|
WorldOffsetY: 0f,
|
|
CurrentCellId: 0u);
|
|
|
|
private static RuntimeCollisionGenerationCommit CommitPrepared(
|
|
RuntimePhysicsState physics,
|
|
RuntimeCollisionAdmission admission,
|
|
PreparedLandblockCollisionGeneration prepared)
|
|
{
|
|
bool engineCommitted = false;
|
|
for (int poll = 0; poll < 10_000; poll++)
|
|
{
|
|
if (!engineCommitted)
|
|
{
|
|
while (!physics.AdvanceCollisionRetainedOwnerCapture(
|
|
admission,
|
|
prepared).Completed)
|
|
{
|
|
}
|
|
foreach (uint ownerId in prepared.RetainedOwnerIds)
|
|
{
|
|
physics.RefreshCollisionRetainedOwner(
|
|
admission,
|
|
prepared,
|
|
ownerId);
|
|
}
|
|
RuntimeCollisionSealStep seal;
|
|
do
|
|
{
|
|
seal = physics.AdvanceCollisionGenerationSeal(
|
|
admission,
|
|
prepared);
|
|
}
|
|
while (!seal.Completed && !seal.Restarted);
|
|
if (!seal.Completed)
|
|
continue;
|
|
}
|
|
RuntimeCollisionGenerationCommit result =
|
|
physics.CommitCollisionGeneration(admission, prepared);
|
|
if (result.Completed)
|
|
return result;
|
|
while (physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot projection))
|
|
{
|
|
Assert.True(physics.SetPosition.AcknowledgeProjection(
|
|
projection.Token));
|
|
}
|
|
engineCommitted = result.EngineCommitted;
|
|
}
|
|
throw new InvalidOperationException(
|
|
"Collision generation did not complete its Runtime mutation transaction.");
|
|
}
|
|
|
|
private static void AddSyntheticCell(
|
|
PhysicsDataCache cache,
|
|
uint cellId)
|
|
{
|
|
cache.RegisterCellStructForTest(
|
|
cellId,
|
|
new CellPhysics
|
|
{
|
|
WorldTransform = Matrix4x4.Identity,
|
|
InverseWorldTransform = Matrix4x4.Identity,
|
|
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
|
|
Portals = [new PortalInfo(0, 0, 0)],
|
|
CellBSP = new CellBSPTree
|
|
{
|
|
Root = new CellBSPNode { Type = BSPNodeType.Leaf },
|
|
},
|
|
});
|
|
cache.CellGraph.Add(new EnvCell(
|
|
cellId,
|
|
Matrix4x4.Identity,
|
|
Matrix4x4.Identity,
|
|
Vector3.Zero,
|
|
Vector3.One,
|
|
Array.Empty<AcDream.Core.World.Cells.CellPortal>(),
|
|
Array.Empty<uint>(),
|
|
seenOutside: false,
|
|
containmentBsp: null));
|
|
}
|
|
|
|
private static void AddFlatLandblock(
|
|
PhysicsEngine engine,
|
|
uint landblock,
|
|
float worldOffsetX)
|
|
{
|
|
engine.AddLandblock(
|
|
landblock,
|
|
new TerrainSurface(new byte[81], new float[256]),
|
|
Array.Empty<CellSurface>(),
|
|
Array.Empty<PortalPlane>(),
|
|
worldOffsetX,
|
|
worldOffsetY: 0f);
|
|
}
|
|
|
|
private static WorldSession.EntitySpawn Spawn(uint guid, ushort instance)
|
|
{
|
|
var position = new CreateObject.ServerPosition(
|
|
SourceCell,
|
|
10f,
|
|
20f,
|
|
7f,
|
|
1f,
|
|
0f,
|
|
0f,
|
|
0f);
|
|
var timestamps = new PhysicsTimestamps(
|
|
Position: 1,
|
|
Movement: 1,
|
|
State: 1,
|
|
Vector: 1,
|
|
Teleport: 0,
|
|
ServerControlledMove: 1,
|
|
ForcePosition: 0,
|
|
ObjDesc: 1,
|
|
Instance: instance);
|
|
var physics = new PhysicsSpawnData(
|
|
RawState: (uint)PhysicsStateFlags.Gravity,
|
|
Position: position,
|
|
Movement: null,
|
|
AnimationFrame: null,
|
|
SetupTableId: 0x02000001u,
|
|
MotionTableId: 0x09000001u,
|
|
SoundTableId: null,
|
|
PhysicsScriptTableId: null,
|
|
Parent: null,
|
|
Children: null,
|
|
Scale: null,
|
|
Friction: null,
|
|
Elasticity: null,
|
|
Translucency: null,
|
|
Velocity: null,
|
|
Acceleration: null,
|
|
AngularVelocity: null,
|
|
DefaultScriptType: null,
|
|
DefaultScriptIntensity: null,
|
|
Timestamps: timestamps);
|
|
return new WorldSession.EntitySpawn(
|
|
guid,
|
|
position,
|
|
0x02000001u,
|
|
Array.Empty<CreateObject.AnimPartChange>(),
|
|
Array.Empty<CreateObject.TextureChange>(),
|
|
Array.Empty<CreateObject.SubPaletteSwap>(),
|
|
null,
|
|
null,
|
|
"set-position-fixture",
|
|
null,
|
|
null,
|
|
0x09000001u,
|
|
PhysicsState: (uint)PhysicsStateFlags.Gravity,
|
|
InstanceSequence: instance,
|
|
MovementSequence: 1,
|
|
ServerControlSequence: 1,
|
|
PositionSequence: 1,
|
|
Physics: physics);
|
|
}
|
|
|
|
private sealed class PlacementObserver(
|
|
Action<RuntimePlacementDelta>? onPlacement = null)
|
|
: IRuntimePlacementObserver
|
|
{
|
|
internal List<RuntimePlacementDelta> Deltas { get; } = [];
|
|
|
|
public void OnPlacement(in RuntimePlacementDelta delta)
|
|
{
|
|
Deltas.Add(delta);
|
|
onPlacement?.Invoke(delta);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// C0-3 test double: a minimal <see cref="IPreparedCollisionSource"/>
|
|
/// serving exactly one Setup id (matching <c>CanonicalSetupTableId</c>'s
|
|
/// field), so <see cref="TryPrepareAndSubmitAuthoredPlacement_ChainsSetupReadThroughPrepareMoverToSubmit"/>
|
|
/// can prove <c>ReadSetupCollision</c> -> <c>PrepareMover</c> ->
|
|
/// <c>SubmitPreparedPlacement</c> actually chains, not merely that each
|
|
/// step works in isolation (the existing preparer tests' coverage).
|
|
/// </summary>
|
|
private sealed class FakeCollisionSource(
|
|
uint expectedSetupTableId,
|
|
FlatSetupCollision? setup,
|
|
PreparedAssetReadStatus status = PreparedAssetReadStatus.Loaded)
|
|
: IPreparedCollisionSource
|
|
{
|
|
internal int ReadCount { get; private set; }
|
|
|
|
public PreparedAssetPresence ProbeCollision(
|
|
PakAssetType type, uint sourceFileId) =>
|
|
PreparedAssetPresence.Available;
|
|
|
|
public PreparedCollisionReadResult<FlatGfxObjCollisionAsset>
|
|
ReadGfxObjCollision(
|
|
uint sourceFileId,
|
|
CancellationToken cancellationToken = default) =>
|
|
throw new NotSupportedException(
|
|
"Only ReadSetupCollision is exercised by C0-3.");
|
|
|
|
public PreparedCollisionReadResult<FlatSetupCollision>
|
|
ReadSetupCollision(
|
|
uint sourceFileId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ReadCount++;
|
|
Assert.Equal(expectedSetupTableId, sourceFileId);
|
|
return status switch
|
|
{
|
|
PreparedAssetReadStatus.Loaded when setup is not null =>
|
|
PreparedCollisionReadResult<FlatSetupCollision>.Loaded(
|
|
setup),
|
|
PreparedAssetReadStatus.Corrupt =>
|
|
PreparedCollisionReadResult<FlatSetupCollision>.Corrupt,
|
|
_ => PreparedCollisionReadResult<FlatSetupCollision>.Missing,
|
|
};
|
|
}
|
|
|
|
public PreparedCollisionReadResult<FlatCellStructureCollisionAsset>
|
|
ReadCellStructureCollision(
|
|
uint sourceFileId,
|
|
CancellationToken cancellationToken = default) =>
|
|
throw new NotSupportedException(
|
|
"Only ReadSetupCollision is exercised by C0-3.");
|
|
|
|
public PreparedCollisionReadResult<FlatEnvCellTopology>
|
|
ReadEnvCellTopology(
|
|
uint sourceFileId,
|
|
CancellationToken cancellationToken = default) =>
|
|
throw new NotSupportedException(
|
|
"Only ReadSetupCollision is exercised by C0-3.");
|
|
|
|
public PreparedCollisionSourceStats CollisionStats =>
|
|
new(ReadCount, ReadCount, ReadCount, 0, 0);
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
|
|
private sealed class CollisionReportObserver
|
|
: IRuntimeCollisionReportObserver
|
|
{
|
|
internal List<RuntimeCollisionReport> Reports { get; } = [];
|
|
|
|
public void OnCollisionReport(in RuntimeCollisionReport report) =>
|
|
Reports.Add(report);
|
|
}
|
|
|
|
private sealed class EntityObserver : IRuntimeEntityObjectObserver
|
|
{
|
|
internal List<RuntimeEntityDelta> Deltas { get; } = [];
|
|
|
|
public void OnEntity(in RuntimeEntityDelta delta) => Deltas.Add(delta);
|
|
|
|
public void OnInventory(in RuntimeInventoryDelta delta)
|
|
{
|
|
}
|
|
}
|
|
|
|
private sealed class ThrowingPlacementObserver : IRuntimePlacementObserver
|
|
{
|
|
public void OnPlacement(in RuntimePlacementDelta delta) =>
|
|
throw new InvalidOperationException("fixture observer failure");
|
|
}
|
|
|
|
private sealed class ReentrantRemotePlacement(PhysicsBody body)
|
|
: IRuntimeRemotePlacement
|
|
{
|
|
private Func<uint>? _readCell;
|
|
private Action<uint>? _writeCell;
|
|
private uint _cellId;
|
|
|
|
public PhysicsBody Body { get; } = body;
|
|
public uint CellId
|
|
{
|
|
get => _readCell?.Invoke() ?? _cellId;
|
|
set
|
|
{
|
|
_cellId = value;
|
|
_writeCell?.Invoke(value);
|
|
}
|
|
}
|
|
public bool Airborne { get; set; }
|
|
public Vector3 LastServerPosition { get; set; }
|
|
public double LastServerPositionTime { get; set; }
|
|
public Vector3 LastShadowSyncPosition { get; set; }
|
|
public Quaternion LastShadowSyncOrientation { get; set; }
|
|
internal Action? OnHitGround { get; init; }
|
|
internal Action? OnLeaveGround { get; init; }
|
|
|
|
public void BindCanonicalCell(Func<uint> read, Action<uint> write)
|
|
{
|
|
_readCell = read;
|
|
_writeCell = write;
|
|
}
|
|
|
|
public void HitGround() => OnHitGround?.Invoke();
|
|
|
|
public void LeaveGround() => OnLeaveGround?.Invoke();
|
|
}
|
|
|
|
private sealed class ManualTimeProvider(DateTimeOffset utcNow)
|
|
: TimeProvider
|
|
{
|
|
private DateTimeOffset _utcNow = utcNow;
|
|
private long _timestamp;
|
|
|
|
public override DateTimeOffset GetUtcNow() => _utcNow;
|
|
public override long TimestampFrequency => TimeSpan.TicksPerSecond;
|
|
public override long GetTimestamp() => _timestamp;
|
|
|
|
internal void Advance(TimeSpan duration)
|
|
{
|
|
_utcNow += duration;
|
|
_timestamp += duration.Ticks;
|
|
}
|
|
|
|
internal void JumpUtc(TimeSpan duration) => _utcNow += duration;
|
|
}
|
|
|
|
public enum DeferredPhysicsStateMutation
|
|
{
|
|
ChildNoDraw,
|
|
StopMissile,
|
|
DirectFinalState,
|
|
}
|
|
|
|
private enum CancellationChannel : uint
|
|
{
|
|
Position,
|
|
Pickup,
|
|
Parent,
|
|
}
|
|
}
|
|
|
|
internal static class RuntimePlacementProjectionTokenTestExtensions
|
|
{
|
|
internal static ulong TokenSequence(
|
|
this RuntimePlacementProjectionToken token) => token.Sequence;
|
|
}
|