The steady-state accepted-Position merge did two things retail never does,
on every single Position packet: it installed the wire placement frame and
unparented unconditionally, and it derived the record's FullCellId from
bare wire acceptance. Both are now correct, and they land together - a
half-flipped intermediate (classified flags with the wire stamp, or vice
versa) is exactly the mixed-residency state this campaign keeps paying for.
WHY the flags need no route. SmartBox::HandleReceivedPosition @0x00453FD0
decides both pre-placement writes BEFORE MoveOrTeleport is consulted: Gate A
@0x0045400C returns @0x0045409D ahead of unset_parent @0x00454129 and ahead
of the HasAnims SetPlacementFrame gate @0x00454137. Neither gate reads the
near/far/teleport classification. So the two flags are a pure function of
(disposition, hasAnimations) and are computable inside the merge, pre-merge,
with no signature change, no route construction and no playerDistance - the
scoping's ~150-400-line route-plumbing estimate over-counted because it did
not see this. That truth table IS
RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition's own
ApplyPlacementFrameBeforeRouting/UnparentBeforeRouting rows; the classifier
stays the oracle and the equality is pinned by test, not by a shared path,
so each computation remains separately sabotage-verifiable.
WHY the cell is withheld. HandleReceivedPosition reads the wire objcell_id
into a LOCAL @0x00453FE3 and hands it only to BlipPlayer / TeleportPlayer /
MoveOrTeleport / ConstrainTo; it never assigns the object's cell. The
object's cell moves inside the placement family (SetPositionInternal
@0x00515BD0 to set_cell, enter_world) or per-frame transit, and nowhere
else. The continuation executor has encoded that rule since the executor
slice; this caller now matches it verbatim.
WHAT DELIBERATELY SURVIVES. Two steady-state wire-cell writers stay,
downstream of the merge and outside the classification window: the
OnPosition prologue rebucket (W2, into CommitRebucket), which is also the
local player's own cell-freshness path, and the post-routing wire-cell adopt
for non-placing arms (W3, AP-135). Gating W2 "for symmetry" would freeze the
player's canonical cell between teleports and #319's child-cell equality
would inherit the freeze. AD-60's rewrite names both so the retirement
cannot be misread as "wire acceptance never changes residency anywhere".
REGISTER. AP-131 RETIRED - the unconditional literals no longer exist; the
caller was corrected, not deleted, so the row's own "deleted at the
production cutover" framing is overtaken. AD-60's legacy half RETIRED and
the row REWRITTEN rather than deleted, naming W2/W3 (route 4b-3's D8
precedent: a silent whole-row deletion would hide surviving channels).
AP-130 amended - the merge consumes the same static HasAnimations proxy,
deliberately not escalated to a live animation-queue read. AP-146 and #320
amended - their "accepted inbound Position (RefreshSnapshot into
RuntimeEntityRecord.cs:234)" local-player cell writer is now the generic
tail's CommitRebucket, and a ForcePosition (which returns before that tail)
is placement-receipt-authoritative. #275 closed.
HEADLINE BEHAVIOURAL DELTA, stated once: a refused or contended local
ForcePosition now leaves FullCellId at the last committed cell where the
merge used to stamp the refused packet's wire cell. Retail cannot refuse
(AD-62) and its body keeps its last placed cell, so the new shape is the
retail-reachable one.
THREE CONSUMER SITES THE CONTRACT'S BLAST-RADIUS SURVEY MISSED, all
D2-caused, all found by the suite rather than by reading, all intended
semantics rather than regressions (recorded in the contract's new section
14):
(1) DatLiveEntityProjectionMaterializer's self-projection branch reads
FullCellId inside OnPosition's prologue recovery, ahead of W2. It now
correctly declines to project from an unplaced wire claim; production
installs the bucket at W2 in the same call (verified: no return between
the recovery call and W2 is conditioned on IsSpatiallyProjected or
FullCellId). Two hydration tests asserted the bucket at the recovery
boundary and now drive the production W2 step - the same shape as trap
T2, one layer up.
(2) ProjectileController.SyncPresentationFromResolvedBody writes
ParentCellId = record.FullCellId. On a refused missile placement that is
now the committed source cell. The MAJOR-1 invariant is unchanged and is
now asserted as the identity it always meant rather than as a wire-cell
constant.
(3) The merge's Rebucketed ternary does NOT become always-Updated as the
contract predicted, and is deliberately kept: the
Forget(restoreCancelledPark: true) above it can roll a wakeable
lost-cell park back, and RestoreParkWithdrawal restores canonical
residency. That is a real cell edge produced inside this method by a
placement owner.
TEST-COUNT RECONCILIATION. Baseline measured at this HEAD by stashing the
change: Runtime.Tests 1176, App.Tests 4135 (4132 passed / 3 skipped),
solution 11,106 passed / 4 skipped - matching the recorded figure at
6921a027 exactly. Post-change: Runtime.Tests 1195, App.Tests 4135 unchanged,
solution 11,125 passed / 4 skipped / 0 failed. Net +19, entirely new Runtime
tests: 3 facts plus a 12-row matrix theory in
InboundPhysicsStateControllerTests, 1 fact plus a 2-row theory in the new
RuntimeSteadyStatePositionMergeTests, and 1 fact in
RuntimeAcceptedPositionDriveControllerTests. No test was deleted; five
existing tests were rewritten in place, never delete-only. No new skip; none
of #302/#308/#321 appeared.
SABOTAGE VERIFICATIONS (each new discriminating test, both directions;
production line broken, suite run, line restored):
installPlacementFrame (!force && !hasAnimations) to (!force)
5 fail: ApplyOnAnimatedEntity_NeverInstallsTheWirePlacementFrame plus
the 4 animated non-force matrix rows.
installPlacementFrame to false
6 fail: ApplyOnNonAnimatedEntity_InstallsTheWirePlacementFrame,
PositionPlacementAbsentAndPresentZeroBothApplyRetailZero plus the 4
non-animated non-force matrix rows.
clearParent (!force) to true
3 fail: ForcePositionOnParentedLocalPlayer_RetainsTheParentAttachment
plus the 2 force+parented matrix rows.
clearParent (!force) to false
4 fail: the 4 Apply+parented matrix rows.
refreshPosition false to acceptedPosition
4 fail: AcceptedPosition_WithholdsTheWireCellAtTheMergeBoundary,
ContendedForcePosition_WritesNoResidencyAnywhere,
ReentrantNewerPositionDuringPickupDiscardSuppressesStalePickupDelta,
MissileFarRefused_...ParentCellIdAgreesWithCommittedCell. Confirmed a
second time by the baseline measurement above, where the withhold test
was the sole red.
CommitRebucket publishes Updated instead of Rebucketed
2 fail: both parent classes of
CellChangingAcceptedPosition_ConservesOneRebucketAndOneChildPropagation.
RuntimeEntityDirectory.SetFullCell drops PropagateFullCellToChildren
2 fail: the same two rows.
T4 respected: the ForcePosition placement-frame half is inert
(appliedPlacement keeps old.PlacementId under either flag value), so the
force row's discriminating assertion is parent retention, never the frame.
NOT DONE, deliberately: the executor is still not wired into the
steady-state path (#275's alternative branch); W2/W3 are untouched; no probe
added or stripped; AP-130's proxy not escalated; no while-here unification
of the two merge callsites. No automated OnPosition-level test drives the
full pickup / drop / reproject sequence (no fixture covers pickup at that
layer); the contract's connected gate recipe item 1 is the positive evidence
for it and has NOT been run - this commit is not connected-gated.
Contract: docs/research/2026-08-05-c5b-contract.md (committed here, with its
section 14 implementation outcome appended).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3741 lines
162 KiB
C#
3741 lines
162 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>
|
|
/// 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;
|
|
}
|