Shipped-code defect affecting committed route-2 code, found while reviewing route 4b-1. RuntimeSetPositionState.ParkDeferred withdraws an entity from the world: body.InWorld = false, TransientStateFlags.Active cleared, WithdrawCanonical, SuspendObjectClock. CancelCoreDeferred then removed the operation and rewrote the pending Withdraw into a Discard while restoring NONE of it. So cancelling a wakeable park was strictly worse than keeping one — the park is wakeable, the cancel destroys the only object that could ever wake it, and the entity is left invisible AND intangible with nothing to bring it back. Route 2's re-issue funnel masked this: re-issuing is correct for a one-shot ForcePosition ACE never repeats, and wrong for a repeated remote stream, so the hole was hidden rather than fixed. Retail's own answer is a working park, verified in the decomp rather than assumed: CPhysicsObj::SetPositionInternal @0x00515BD0, when AdjustPosition yields no cell @0x00515C1D, calls prepare_to_leave_visibility @0x00515CDA, store_position @0x00515CE2 (the DESTINATION pose is committed), GotoLostCell @0x00515CF2 registering at m_position.objcell_id read AFTER store_position (so the destination cell), clears transient 0x80 @0x00515CF7, and returns OK @0x00515D07. InitObjCell @0x00508260 drains the lost list on cell load and calls reenter_visibility @0x00516250, which re-places from the object's OWN m_position with flags 0x11. Two corrections to the direction I gave, both forced by evidence and both right: The pose must NOT be rolled back — only the withdrawal. Three shipped route-2 tests capture positionAtPark AFTER the park and assert it survives the cancel, and retail agrees: store_position commits the destination and nothing un-commits it. Restoring residency at the body's committed cell is therefore retail's own cell choice, not merely self-consistent. The gate defaults to FALSE with four explicit opt-ins, rather than defaulting true with opt-outs at the withdrawal callers. That keeps every one of the ~20 shipped Forget/ForgetExactPlacement sites at exactly its current behaviour instead of depending on having correctly enumerated the withdrawal transactions. Review had already found the broad version corrupting five of them (TryApplyPickup, CommitAcceptedParent, CommitAcceptedParentCellless, CommitWithdrawal, CommitPositionChannelUpdate): they hand-roll a partial re-withdrawal that undoes the clock and FullCellId but not InWorld or the _spatialRoots re-registration, leaving a picked-up item both in inventory and an InWorld cellless spatial root in the physics workset. ParkDeferred's restorableOnCancel is opt-in for exactly one of its four callers — the plain unplaceable-destination park. Every quiescence and retirement park is excluded deliberately: those entities are withdrawn because their world is going away, and restoring residency inside a quiescing prefix blocks its retirement. VerifyPositionChannelCancellation now asserts InWorld and IsSpatialRoot per channel — Position is a cancellation and must restore; Pickup and Parent are withdrawals and must not. It previously asserted only !IsDeferred and counts, which is why five green states hid this. Register row AP-136 measured against GotoLostCell/reenter_visibility rather than labelled "retail-shaped". Files #309 (the restore-on-cancel residual, with park-survives recorded as the retail-faithful target and its two blockers named: the NewerPositionPickupAndParentEachCancelExactLostOperation invariant and teardown convergence) and #310 (an unbounded retirement stall — a retained preparation retry pins its prefix through HasOldPrefixPlacementDebt forever, and TickLostCellDeadlines has no production caller so the 25 s timer never fires). This is a user-observable change to shipped paths: restorableOnCancel: true sits in SubmitPreparedPlacementCore, the shared core behind every production placement. AP-136 and #309 carry the proposed two-client check. Gates: complete Release solution 10,973 passed / 4 skipped / 0 failed (baseline 10,938). Every new test discrimination-verified by reverting the fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1129 lines
44 KiB
C#
1129 lines
44 KiB
C#
using System.Collections.Immutable;
|
|
using System.Numerics;
|
|
using AcDream.Core.Net;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Runtime.Entities;
|
|
using AcDream.Runtime.Physics;
|
|
using DatReaderWriter.Enums;
|
|
using DatReaderWriter.Types;
|
|
|
|
namespace AcDream.Runtime.Tests.Physics;
|
|
|
|
public sealed partial class RuntimeCollisionPrefixQuiescenceTests
|
|
{
|
|
private const uint PrefixP = 0xA9B40000u;
|
|
private const uint CellP = PrefixP | 0x0001u;
|
|
private const uint PrefixQ = 0xAAB40000u;
|
|
private const uint CellQ = PrefixQ | 0x0001u;
|
|
|
|
[Fact]
|
|
public void EarlierUnrelatedFifoHeadDrainsBeforeEveryResidentWithdraw()
|
|
{
|
|
using var fixture = new Fixture();
|
|
RuntimeEntityRecord unrelated = fixture.Add(
|
|
0x70003001u,
|
|
1,
|
|
CellQ,
|
|
new Vector3(10f, 20f, 7f));
|
|
RuntimeEntityRecord first = fixture.Add(
|
|
0x70003002u,
|
|
1,
|
|
CellP,
|
|
new Vector3(11f, 20f, 7f));
|
|
RuntimeEntityRecord second = fixture.Add(
|
|
0x70003003u,
|
|
1,
|
|
CellP,
|
|
new Vector3(12f, 20f, 7f));
|
|
RuntimeSetPositionOutcome unrelatedPlace = fixture.Place(
|
|
unrelated,
|
|
CellQ,
|
|
new Vector3(13f, 20f, 7f));
|
|
RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL);
|
|
|
|
Assert.False(fixture.TryAcquire(token, out _));
|
|
Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(first));
|
|
Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(second));
|
|
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(unrelatedPlace.Projection));
|
|
Assert.False(fixture.TryAcquire(token, out _));
|
|
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(first));
|
|
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(second));
|
|
|
|
var withdrawals = new List<RuntimePlacementProjectionToken>();
|
|
while (fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot pending))
|
|
{
|
|
Assert.Equal(RuntimePlacementProjectionKind.Withdraw, pending.Kind);
|
|
withdrawals.Add(pending.Token);
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(pending.Token));
|
|
}
|
|
|
|
Assert.Equal(2, withdrawals.Count);
|
|
Assert.True(fixture.TryAcquire(token, out var permission));
|
|
Assert.Equal(withdrawals, permission.Withdrawals);
|
|
Assert.True(fixture.Lifetime.Physics
|
|
.IsCollisionPrefixMutationPermissionCurrent(permission));
|
|
Assert.True(fixture.Lifetime.Physics
|
|
.CancelCollisionPrefixQuiescence(
|
|
token,
|
|
successorGeneration: 2UL,
|
|
successorReady: false));
|
|
Assert.False(fixture.Lifetime.Physics
|
|
.IsCollisionPrefixMutationPermissionCurrent(permission));
|
|
}
|
|
|
|
[Fact]
|
|
public void ThrowAndFalseSinkKeepExactWithdrawRetryableUntilAck()
|
|
{
|
|
using var fixture = new Fixture(bindGeneration: true);
|
|
_ = fixture.Add(
|
|
0x70003004u,
|
|
1,
|
|
CellP,
|
|
new Vector3(10f, 21f, 7f));
|
|
int attempt = 0;
|
|
var sink = new RecordingSink(_ => ++attempt switch
|
|
{
|
|
1 => throw new InvalidOperationException("host unavailable"),
|
|
2 => false,
|
|
_ => true,
|
|
});
|
|
using var subscription = fixture.Subscribe(sink);
|
|
RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL);
|
|
|
|
Assert.False(fixture.TryAcquire(token, out _));
|
|
Assert.Equal(1, fixture.Lifetime.Placements.PendingCount);
|
|
Assert.False(fixture.TryAcquire(token, out _));
|
|
Assert.Equal(1, fixture.Lifetime.Events.DispatchFailureCount);
|
|
|
|
Assert.True(subscription.RetryPending());
|
|
Assert.Equal(1, fixture.Lifetime.Placements.PendingCount);
|
|
Assert.True(subscription.RetryPending());
|
|
Assert.Equal(0, fixture.Lifetime.Placements.PendingCount);
|
|
|
|
Assert.True(fixture.TryAcquire(token, out var permission));
|
|
Assert.Single(permission.Withdrawals);
|
|
Assert.Equal(3, sink.Applied.Count);
|
|
Assert.All(sink.Applied,
|
|
projection => Assert.Equal(
|
|
permission.Withdrawals[0].Sequence,
|
|
projection.Token.Sequence));
|
|
}
|
|
|
|
[Fact]
|
|
public void SourceToOutsidePlacementIsHeldThenRestoredBeforeBarrierOpens()
|
|
{
|
|
using var fixture = new Fixture();
|
|
RuntimeEntityRecord record = fixture.Add(
|
|
0x70003005u,
|
|
1,
|
|
CellP,
|
|
new Vector3(10f, 22f, 7f));
|
|
RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL);
|
|
|
|
RuntimeSetPositionOutcome held = fixture.Place(
|
|
record,
|
|
CellQ,
|
|
new Vector3(14f, 22f, 7f),
|
|
currentCell: CellP);
|
|
|
|
Assert.Equal(RuntimeSetPositionStatus.DeferredCell, held.Status);
|
|
Assert.Equal(2UL, held.Projection.CollisionGeneration);
|
|
Assert.Equal(CellQ, held.ExactCellId);
|
|
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(record));
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(held.Projection));
|
|
Assert.False(fixture.TryAcquire(token, out _));
|
|
Assert.True(fixture.TryAcquire(token, out _));
|
|
|
|
Assert.False(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence(
|
|
token,
|
|
successorGeneration: 1UL,
|
|
successorReady: true));
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot restored));
|
|
Assert.Equal(RuntimePlacementProjectionKind.Place, restored.Kind);
|
|
Assert.Equal(CellQ, restored.Token.ExactCellId);
|
|
Assert.False(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence(
|
|
token,
|
|
successorGeneration: 1UL,
|
|
successorReady: true));
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(restored.Token));
|
|
Assert.True(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence(
|
|
token,
|
|
successorGeneration: 1UL,
|
|
successorReady: true));
|
|
Assert.Equal(CellQ, record.FullCellId);
|
|
Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(record));
|
|
}
|
|
|
|
[Fact]
|
|
public void OutsideToTargetPlacementIsHeldWithoutEvaluatingRetiringRows()
|
|
{
|
|
using var fixture = new Fixture();
|
|
RuntimeEntityRecord record = fixture.Add(
|
|
0x70003006u,
|
|
1,
|
|
CellQ,
|
|
new Vector3(10f, 23f, 7f));
|
|
RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL);
|
|
|
|
RuntimeSetPositionOutcome held = fixture.Place(
|
|
record,
|
|
CellP,
|
|
new Vector3(15f, 23f, 7f),
|
|
currentCell: CellQ);
|
|
|
|
Assert.Equal(RuntimeSetPositionStatus.DeferredCell, held.Status);
|
|
Assert.Equal(CellP, held.ExactCellId);
|
|
Assert.Equal(2UL, held.Projection.CollisionGeneration);
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(held.Projection));
|
|
Assert.False(fixture.TryAcquire(token, out _));
|
|
Assert.True(fixture.TryAcquire(token, out _));
|
|
|
|
Assert.False(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence(
|
|
token,
|
|
successorGeneration: 1UL,
|
|
successorReady: true));
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot restored));
|
|
Assert.Equal(CellP, restored.Token.ExactCellId);
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(restored.Token));
|
|
Assert.True(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence(
|
|
token,
|
|
successorGeneration: 1UL,
|
|
successorReady: true));
|
|
Assert.Equal(restored.Token.ExactCellId, record.FullCellId);
|
|
}
|
|
|
|
[Fact]
|
|
public void SupersessionTransfersParkedReceiptAndOnlySuccessorCanAcquire()
|
|
{
|
|
using var fixture = new Fixture();
|
|
_ = fixture.Add(
|
|
0x70003007u,
|
|
1,
|
|
CellP,
|
|
new Vector3(10f, 24f, 7f));
|
|
RuntimeCollisionPrefixQuiescenceToken first = fixture.Begin(2UL);
|
|
Assert.False(fixture.TryAcquire(first, out _));
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot withdrawal));
|
|
|
|
RuntimeCollisionPrefixQuiescenceToken successor = fixture.Begin(3UL);
|
|
|
|
Assert.False(fixture.TryAcquire(first, out _));
|
|
Assert.False(fixture.TryAcquire(successor, out _));
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(withdrawal.Token));
|
|
Assert.True(fixture.TryAcquire(successor, out var permission));
|
|
Assert.Equal(successor, permission.Quiescence);
|
|
Assert.Single(permission.Withdrawals);
|
|
Assert.Equal(withdrawal.Token.Sequence,
|
|
permission.Withdrawals[0].Sequence);
|
|
}
|
|
|
|
[Fact]
|
|
public void ResetAndDisposeConvergePrefixBarrierOwnership()
|
|
{
|
|
var fixture = new Fixture();
|
|
_ = fixture.Add(
|
|
0x70003008u,
|
|
1,
|
|
CellP,
|
|
new Vector3(10f, 25f, 7f));
|
|
RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL);
|
|
Assert.False(fixture.TryAcquire(token, out _));
|
|
Assert.Equal(1, fixture.Lifetime.Physics.CaptureOwnership()
|
|
.CollisionPrefixQuiescenceCount);
|
|
|
|
fixture.Lifetime.Physics.SetPosition.ResetSession();
|
|
|
|
RuntimePhysicsOwnershipSnapshot reset =
|
|
fixture.Lifetime.Physics.CaptureOwnership();
|
|
Assert.Equal(0, reset.CollisionPrefixQuiescenceCount);
|
|
Assert.Equal(0, reset.PendingCollisionPrefixProjectionCount);
|
|
fixture.Dispose();
|
|
Assert.True(fixture.Lifetime.Physics.CaptureOwnership().IsConverged);
|
|
}
|
|
|
|
[Fact]
|
|
public void RevisedDiscardRetainsExactBarrierDebtUntilDiscardAck()
|
|
{
|
|
using var fixture = new Fixture();
|
|
RuntimeEntityRecord record = fixture.Add(
|
|
0x70003009u,
|
|
1,
|
|
CellP,
|
|
new Vector3(10f, 26f, 7f));
|
|
RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL);
|
|
|
|
Assert.False(fixture.TryAcquire(token, out _));
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot withdrawal));
|
|
Assert.Equal(RuntimePlacementProjectionKind.Withdraw, withdrawal.Kind);
|
|
|
|
RuntimePlacementCancellationReceipt cancellation = fixture.Lifetime
|
|
.Physics.SetPosition.Forget(record);
|
|
Assert.True(cancellation.IsValid);
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot discard));
|
|
Assert.Equal(RuntimePlacementProjectionKind.Discard, discard.Kind);
|
|
Assert.Equal(withdrawal.Token.Sequence, discard.Token.Sequence);
|
|
Assert.True(discard.Token.Revision > withdrawal.Token.Revision);
|
|
Assert.False(fixture.TryAcquire(token, out _));
|
|
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(discard.Token));
|
|
Assert.True(fixture.TryAcquire(token, out var permission));
|
|
Assert.Single(permission.Withdrawals);
|
|
Assert.Equal(discard.Token, permission.Withdrawals[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public void AcceptedPreparationDebtKeepsParkingFailAtomicAndTokenRetryable()
|
|
{
|
|
using var fixture = new Fixture();
|
|
RuntimeEntityRecord record = fixture.Add(
|
|
0x7000300Au,
|
|
1,
|
|
CellP,
|
|
new Vector3(10f, 27f, 7f));
|
|
RuntimeEntityPlacementToken placement = fixture.Lifetime.Physics
|
|
.SetPosition.BeginAcceptedPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL);
|
|
|
|
Assert.False(fixture.TryAcquire(token, out _));
|
|
Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(record));
|
|
Assert.False(fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out _));
|
|
Assert.True(fixture.Lifetime.Physics
|
|
.CancelCollisionPrefixQuiescence(token));
|
|
|
|
RuntimeSetPositionOutcome submitted = fixture.Lifetime.Physics
|
|
.SetPosition.SubmitPreparedPlacement(
|
|
placement,
|
|
Command(CellP, new Vector3(11f, 27f, 7f), CellP));
|
|
Assert.Equal(
|
|
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
|
|
submitted.Status);
|
|
}
|
|
|
|
/// <summary>
|
|
/// C4 route 4b-1 review finding B2, pinned. The contract asked whether
|
|
/// <c>ParkCollisionResidents</c>'s overlap throw stays unreachable under a
|
|
/// design that lets remotes hold placement operations. It does, and for a
|
|
/// STRUCTURAL reason rather than an incidental one:
|
|
/// <c>TryAcquireCollisionPrefixMutationPermission</c> consults
|
|
/// <c>HasOldPrefixPlacementDebt</c> FIRST and refuses permission outright,
|
|
/// so <c>ParkCollisionResidents</c> is never entered while any affected
|
|
/// root holds an operation. (Additionally, a parked record has
|
|
/// <c>FullCellId == 0</c>, so it is not an affected resident at all.)
|
|
///
|
|
/// <para><b>The real hazard is not a throw, it is an unbounded streaming
|
|
/// stall — and this test PINS it as a stall, not as a pass.</b> A retained
|
|
/// preparation retry keeps its landblock prefix in placement debt, so the
|
|
/// retirement is refused on EVERY poll and the landblock never retires.
|
|
/// There is no bound: the retirement coordinator simply retries, and
|
|
/// <c>TickLostCellDeadlines</c> — the only expiry that could break the
|
|
/// cycle — has NO production caller, so its deadline never fires. The only
|
|
/// thing that clears it is an inbound packet for that same entity, which
|
|
/// is exactly what a <c>RetrySetupUnavailable</c> on an asset that never
|
|
/// loads does not produce.</para>
|
|
///
|
|
/// <para>This is a pre-existing hazard independent of route 4b-1, filed as
|
|
/// its own issue. 4b-1 does NOT bound it; it only avoids widening it, by
|
|
/// declining to retain operations for destinations it cannot service.</para>
|
|
/// </summary>
|
|
[Fact]
|
|
public void RetainedPreparationRetryStallsPrefixRetirementIndefinitely()
|
|
{
|
|
using var fixture = new Fixture();
|
|
RuntimeEntityRecord record = fixture.Add(
|
|
0x7000300Bu,
|
|
1,
|
|
CellP,
|
|
new Vector3(10f, 28f, 7f));
|
|
|
|
// A retained preparation retry: begun, never prepared — the shape a
|
|
// RetrySetupUnavailable on an asset that never resolves leaves behind.
|
|
RuntimeEntityPlacementToken retained = fixture.Lifetime.Physics
|
|
.SetPosition.BeginAcceptedPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
Assert.True(retained.IsValid);
|
|
|
|
RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL);
|
|
|
|
// The production retirement path, polled hard. Permission is refused
|
|
// every single time; nothing in the system advances it.
|
|
for (int poll = 0; poll < 1_000; poll++)
|
|
{
|
|
Assert.False(
|
|
fixture.TryAcquire(token, out _),
|
|
$"retirement unexpectedly acquired permission on poll {poll}; "
|
|
+ "if this now succeeds the stall has been bounded and "
|
|
+ "this test's pinned decision must be revisited");
|
|
}
|
|
|
|
// ParkCollisionResidents was never entered, so its overlap throw could
|
|
// not fire — the contract's item 6, proven structurally.
|
|
Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(record));
|
|
Assert.False(fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out _));
|
|
|
|
// And the discriminator: retiring the retained operation is what
|
|
// releases the prefix. Once the debt is gone the ordinary two-phase
|
|
// handshake proceeds — ParkCollisionResidents withdraws the residents
|
|
// and permission follows the withdrawal acknowledgements — so drain
|
|
// those exactly as the production host does.
|
|
_ = fixture.Lifetime.Physics.SetPosition.ForgetExactPlacement(retained);
|
|
bool acquired = false;
|
|
for (int poll = 0; poll < 32 && !acquired; poll++)
|
|
{
|
|
acquired = fixture.TryAcquire(token, out _);
|
|
if (acquired)
|
|
break;
|
|
if (fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot projection))
|
|
{
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(projection.Token));
|
|
}
|
|
}
|
|
Assert.True(
|
|
acquired,
|
|
"clearing the retained preparation retry must let the prefix "
|
|
+ "retire; if it does not, the stall has a second cause");
|
|
}
|
|
|
|
[Fact]
|
|
public void QueriedNeighborPrefixHoldsResultWithoutRequestDependency()
|
|
{
|
|
uint start = PrefixQ | 0x0101u;
|
|
uint rejected = PrefixP | 0x0101u;
|
|
uint winner = PrefixQ | 0x0102u;
|
|
PhysicsEngine engine = FlatEngine();
|
|
PhysicsDataCache cache = engine.DataCache!;
|
|
cache.RegisterCellStructForTest(
|
|
start,
|
|
ContainmentCell(
|
|
new Plane(new Vector3(0f, -1f, 0f), 3f),
|
|
[rejected, winner]));
|
|
cache.RegisterCellStructForTest(
|
|
rejected,
|
|
ContainmentCell(
|
|
new Plane(new Vector3(0f, 1f, 0f), -20f),
|
|
[]));
|
|
cache.RegisterCellStructForTest(
|
|
winner,
|
|
ContainmentCell(
|
|
new Plane(new Vector3(0f, 1f, 0f), -7f),
|
|
[]));
|
|
using var fixture = new Fixture(engine: engine);
|
|
RuntimeEntityRecord record = fixture.Add(
|
|
0x7000300Bu,
|
|
1,
|
|
start,
|
|
new Vector3(0f, 8f, 1f));
|
|
RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL);
|
|
|
|
RuntimeSetPositionOutcome held = fixture.Place(
|
|
record,
|
|
start,
|
|
new Vector3(0f, 8f, 1f));
|
|
|
|
Assert.Equal(RuntimeSetPositionStatus.DeferredCell, held.Status);
|
|
Assert.Equal(winner, held.ExactCellId);
|
|
Assert.Equal(2UL, held.Projection.CollisionGeneration);
|
|
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(record));
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(held.Projection));
|
|
Assert.False(fixture.TryAcquire(token, out _));
|
|
Assert.True(fixture.TryAcquire(token, out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void SimultaneousPrefixesParkAndDrainIndependentlyInOneFifo()
|
|
{
|
|
using var fixture = new Fixture();
|
|
RuntimeEntityRecord first = fixture.Add(
|
|
0x7000300Cu,
|
|
1,
|
|
CellP,
|
|
new Vector3(10f, 28f, 7f));
|
|
RuntimeEntityRecord second = fixture.Add(
|
|
0x7000300Du,
|
|
1,
|
|
CellQ,
|
|
new Vector3(11f, 28f, 7f));
|
|
RuntimeCollisionPrefixQuiescenceToken p = fixture.Begin(
|
|
PrefixP,
|
|
2UL,
|
|
includeOutdoorCells: true);
|
|
RuntimeCollisionPrefixQuiescenceToken q = fixture.Begin(
|
|
PrefixQ,
|
|
3UL,
|
|
includeOutdoorCells: true);
|
|
|
|
Assert.False(fixture.TryAcquire(p, out _));
|
|
Assert.False(fixture.TryAcquire(q, out _));
|
|
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(first));
|
|
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(second));
|
|
|
|
while (fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot pending))
|
|
{
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(pending.Token));
|
|
}
|
|
|
|
Assert.True(fixture.TryAcquire(p, out var pPermission));
|
|
Assert.True(fixture.TryAcquire(q, out var qPermission));
|
|
Assert.Single(pPermission.Withdrawals);
|
|
Assert.Single(qPermission.Withdrawals);
|
|
Assert.NotEqual(
|
|
pPermission.Withdrawals[0].Sequence,
|
|
qPermission.Withdrawals[0].Sequence);
|
|
}
|
|
|
|
[Fact]
|
|
public void PermissionIsInvalidatedByNewPlacementAfterAcquire()
|
|
{
|
|
using var fixture = new Fixture();
|
|
RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL);
|
|
Assert.False(fixture.TryAcquire(token, out _));
|
|
Assert.True(fixture.TryAcquire(token, out var permission));
|
|
Assert.True(fixture.Lifetime.Physics
|
|
.IsCollisionPrefixMutationPermissionCurrent(permission));
|
|
RuntimeEntityRecord record = fixture.Add(
|
|
0x7000300Eu,
|
|
1,
|
|
CellQ,
|
|
new Vector3(10f, 29f, 7f));
|
|
|
|
RuntimeSetPositionOutcome held = fixture.Place(
|
|
record,
|
|
CellP,
|
|
new Vector3(12f, 29f, 7f),
|
|
currentCell: CellQ);
|
|
|
|
Assert.Equal(RuntimeSetPositionStatus.DeferredCell, held.Status);
|
|
Assert.False(fixture.Lifetime.Physics
|
|
.IsCollisionPrefixMutationPermissionCurrent(permission));
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(held.Projection));
|
|
Assert.True(fixture.TryAcquire(token, out var replacement));
|
|
Assert.NotEqual(permission.Withdrawals, replacement.Withdrawals);
|
|
}
|
|
|
|
[Fact]
|
|
public void IndoorOnlyQuiescenceLeavesOutdoorResidentActive()
|
|
{
|
|
uint indoor = PrefixP | 0x0101u;
|
|
using var fixture = new Fixture();
|
|
RuntimeEntityRecord outdoor = fixture.Add(
|
|
0x7000300Fu,
|
|
1,
|
|
CellP,
|
|
new Vector3(10f, 30f, 7f));
|
|
RuntimeEntityRecord interior = fixture.Add(
|
|
0x70003010u,
|
|
1,
|
|
indoor,
|
|
new Vector3(11f, 30f, 7f));
|
|
RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(
|
|
PrefixP,
|
|
2UL,
|
|
includeOutdoorCells: false);
|
|
|
|
Assert.False(fixture.TryAcquire(token, out _));
|
|
Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(outdoor));
|
|
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(interior));
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot withdrawal));
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(withdrawal.Token));
|
|
Assert.True(fixture.TryAcquire(token, out var permission));
|
|
Assert.Single(permission.Withdrawals);
|
|
Assert.Equal(interior.Key, permission.Withdrawals[0].Entity);
|
|
}
|
|
|
|
[Fact]
|
|
public void WithdrawCallbackDeleteGuidReuseAndNewPlacementCannotEscapeBarrier()
|
|
{
|
|
using var fixture = new Fixture();
|
|
RuntimeEntityRecord first = fixture.Add(
|
|
0x70003011u,
|
|
1,
|
|
CellP,
|
|
new Vector3(10f, 31f, 7f));
|
|
RuntimeEntityRecord retired = fixture.Add(
|
|
0x70003012u,
|
|
1,
|
|
CellP,
|
|
new Vector3(11f, 31f, 7f));
|
|
RuntimeEntityRecord? replacement = null;
|
|
RuntimeSetPositionOutcome reentrant = default;
|
|
var observer = new PlacementObserver(delta =>
|
|
{
|
|
if (replacement is not null
|
|
|| delta.Placement.Kind
|
|
is not RuntimePlacementProjectionKind.Withdraw
|
|
|| delta.Placement.Token.Entity != first.Key)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Assert.True(fixture.Lifetime.TryAcceptDelete(
|
|
new DeleteObject.Parsed(retired.ServerGuid, retired.Incarnation),
|
|
isLocalPlayer: false,
|
|
removeRetainedObject: false,
|
|
out RuntimeEntityDeleteAcceptance acceptance));
|
|
fixture.Lifetime.CompleteAcceptedDelete(acceptance);
|
|
Assert.Null(fixture.Lifetime.RetireCanonicalOnly(retired));
|
|
replacement = fixture.Add(
|
|
retired.ServerGuid,
|
|
2,
|
|
CellQ,
|
|
new Vector3(12f, 31f, 7f));
|
|
reentrant = fixture.Place(
|
|
replacement,
|
|
CellP,
|
|
new Vector3(13f, 31f, 7f),
|
|
currentCell: CellQ);
|
|
});
|
|
using IDisposable subscription = fixture.Lifetime.Events
|
|
.SubscribePlacement(observer);
|
|
RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL);
|
|
|
|
Assert.False(fixture.TryAcquire(token, out _));
|
|
Assert.NotNull(replacement);
|
|
Assert.Equal(RuntimeSetPositionStatus.DeferredCell, reentrant.Status);
|
|
Assert.False(fixture.Lifetime.Entities.IsCurrent(retired));
|
|
Assert.True(fixture.Lifetime.Entities.IsCurrent(replacement));
|
|
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(replacement));
|
|
|
|
int receipts = 0;
|
|
while (fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot pending))
|
|
{
|
|
receipts++;
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(pending.Token));
|
|
}
|
|
Assert.Equal(3, receipts);
|
|
Assert.True(fixture.TryAcquire(token, out var permission));
|
|
Assert.Equal(3, permission.Withdrawals.Length);
|
|
}
|
|
|
|
[Fact]
|
|
public void AbortReleaseKeepsNewcomerBehindBarrierUntilEveryPlaceAck()
|
|
{
|
|
using var fixture = new Fixture();
|
|
RuntimeEntityRecord resident = fixture.Add(
|
|
0x70003013u,
|
|
1,
|
|
CellP,
|
|
new Vector3(10f, 32f, 7f));
|
|
RuntimeSetPositionOutcome prepared = fixture.Place(
|
|
resident,
|
|
CellP,
|
|
new Vector3(11f, 32f, 7f),
|
|
currentCell: CellP);
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(prepared.Projection));
|
|
RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL);
|
|
Assert.False(fixture.TryAcquire(token, out _));
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot residentWithdrawal));
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(residentWithdrawal.Token));
|
|
Assert.True(fixture.TryAcquire(token, out _));
|
|
|
|
Assert.False(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence(
|
|
token,
|
|
successorGeneration: 1UL,
|
|
successorReady: true));
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot residentPlace));
|
|
Assert.Equal(RuntimePlacementProjectionKind.Place, residentPlace.Kind);
|
|
|
|
RuntimeEntityRecord newcomer = fixture.Add(
|
|
0x70003014u,
|
|
1,
|
|
CellQ,
|
|
new Vector3(12f, 32f, 7f));
|
|
RuntimeSetPositionOutcome newcomerHeld = fixture.Place(
|
|
newcomer,
|
|
CellP,
|
|
new Vector3(13f, 32f, 7f),
|
|
currentCell: CellQ);
|
|
Assert.Equal(RuntimeSetPositionStatus.DeferredCell, newcomerHeld.Status);
|
|
Assert.False(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence(
|
|
token,
|
|
successorGeneration: 1UL,
|
|
successorReady: true));
|
|
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(residentPlace.Token));
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot newcomerWithdrawal));
|
|
Assert.Equal(
|
|
RuntimePlacementProjectionKind.Withdraw,
|
|
newcomerWithdrawal.Kind);
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(newcomerWithdrawal.Token));
|
|
Assert.False(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence(
|
|
token,
|
|
successorGeneration: 1UL,
|
|
successorReady: true));
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot newcomerPlace));
|
|
Assert.Equal(RuntimePlacementProjectionKind.Place, newcomerPlace.Kind);
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(newcomerPlace.Token));
|
|
Assert.True(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence(
|
|
token,
|
|
successorGeneration: 1UL,
|
|
successorReady: true));
|
|
Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(resident));
|
|
Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(newcomer));
|
|
}
|
|
|
|
[Fact]
|
|
public void ColdAbortRestoreWaitsForExactMoverPreparation()
|
|
{
|
|
using var fixture = new Fixture();
|
|
RuntimeEntityRecord resident = fixture.Add(
|
|
0x70003015u,
|
|
1,
|
|
CellP,
|
|
new Vector3(10f, 33f, 7f));
|
|
RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL);
|
|
Assert.False(fixture.TryAcquire(token, out _));
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot withdrawal));
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(withdrawal.Token));
|
|
Assert.True(fixture.TryAcquire(token, out _));
|
|
|
|
Assert.False(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence(
|
|
token,
|
|
successorGeneration: 1UL,
|
|
successorReady: true));
|
|
Assert.False(fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out _));
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.TryGetAwaitingPreparationToken(
|
|
resident,
|
|
out RuntimeEntityPlacementToken preparationToken));
|
|
RuntimeSetPositionCommand command = PrepareAuthoredCommand(
|
|
fixture.Lifetime,
|
|
preparationToken);
|
|
|
|
RuntimeSetPositionOutcome submitted = fixture.Lifetime.Physics
|
|
.SetPosition.SubmitPreparedPlacement(preparationToken, command);
|
|
|
|
Assert.Equal(RuntimeSetPositionStatus.DeferredCell, submitted.Status);
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot restored));
|
|
Assert.Equal(RuntimePlacementProjectionKind.Place, restored.Kind);
|
|
Assert.False(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence(
|
|
token,
|
|
successorGeneration: 1UL,
|
|
successorReady: true));
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(restored.Token));
|
|
Assert.True(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence(
|
|
token,
|
|
successorGeneration: 1UL,
|
|
successorReady: true));
|
|
}
|
|
|
|
[Fact]
|
|
public void ResetConvergesPendingDiscardAndPendingRestoreReceipts()
|
|
{
|
|
using (var discardFixture = new Fixture())
|
|
{
|
|
RuntimeEntityRecord record = discardFixture.Add(
|
|
0x70003016u,
|
|
1,
|
|
CellP,
|
|
new Vector3(10f, 34f, 7f));
|
|
RuntimeCollisionPrefixQuiescenceToken token =
|
|
discardFixture.Begin(2UL);
|
|
Assert.False(discardFixture.TryAcquire(token, out _));
|
|
_ = discardFixture.Lifetime.Physics.SetPosition.Forget(record);
|
|
Assert.Equal(1, discardFixture.Lifetime.Physics.CaptureOwnership()
|
|
.PendingCollisionPrefixProjectionCount);
|
|
|
|
discardFixture.Lifetime.Physics.SetPosition.ResetSession();
|
|
|
|
RuntimePhysicsOwnershipSnapshot reset = discardFixture.Lifetime
|
|
.Physics.CaptureOwnership();
|
|
Assert.Equal(0, reset.CollisionPrefixQuiescenceCount);
|
|
Assert.Equal(0, reset.PendingCollisionPrefixProjectionCount);
|
|
}
|
|
|
|
using var restoreFixture = new Fixture();
|
|
RuntimeEntityRecord resident = restoreFixture.Add(
|
|
0x70003017u,
|
|
1,
|
|
CellP,
|
|
new Vector3(10f, 35f, 7f));
|
|
RuntimeSetPositionOutcome prepared = restoreFixture.Place(
|
|
resident,
|
|
CellP,
|
|
new Vector3(11f, 35f, 7f),
|
|
currentCell: CellP);
|
|
Assert.True(restoreFixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(prepared.Projection));
|
|
RuntimeCollisionPrefixQuiescenceToken restoreToken =
|
|
restoreFixture.Begin(2UL);
|
|
Assert.False(restoreFixture.TryAcquire(restoreToken, out _));
|
|
Assert.True(restoreFixture.Lifetime.Physics.SetPosition
|
|
.TryPeekProjection(out RuntimePlacementProjectionSnapshot withdraw));
|
|
Assert.True(restoreFixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(withdraw.Token));
|
|
Assert.True(restoreFixture.TryAcquire(restoreToken, out _));
|
|
Assert.False(restoreFixture.Lifetime.Physics
|
|
.CancelCollisionPrefixQuiescence(
|
|
restoreToken,
|
|
successorGeneration: 1UL,
|
|
successorReady: true));
|
|
Assert.Equal(1, restoreFixture.Lifetime.Physics.CaptureOwnership()
|
|
.PendingCollisionPrefixProjectionCount);
|
|
|
|
restoreFixture.Lifetime.Physics.SetPosition.ResetSession();
|
|
|
|
RuntimePhysicsOwnershipSnapshot restoreReset = restoreFixture.Lifetime
|
|
.Physics.CaptureOwnership();
|
|
Assert.Equal(0, restoreReset.CollisionPrefixQuiescenceCount);
|
|
Assert.Equal(0, restoreReset.PendingCollisionPrefixProjectionCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void CancelBeforeFirstAcquireCannotStrandNewHeldPlacement()
|
|
{
|
|
using var fixture = new Fixture();
|
|
RuntimeEntityRecord record = fixture.Add(
|
|
0x70003018u,
|
|
1,
|
|
CellQ,
|
|
new Vector3(10f, 36f, 7f));
|
|
RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(2UL);
|
|
RuntimeSetPositionOutcome held = fixture.Place(
|
|
record,
|
|
CellP,
|
|
new Vector3(11f, 36f, 7f),
|
|
currentCell: CellQ);
|
|
|
|
Assert.Equal(RuntimeSetPositionStatus.DeferredCell, held.Status);
|
|
Assert.False(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence(
|
|
token));
|
|
Assert.Equal(1, fixture.Lifetime.Physics.CaptureOwnership()
|
|
.CollisionPrefixQuiescenceCount);
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(held.Projection));
|
|
Assert.False(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence(
|
|
token));
|
|
|
|
Assert.False(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence(
|
|
token,
|
|
successorGeneration: 1UL,
|
|
successorReady: true));
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot restored));
|
|
Assert.Equal(RuntimePlacementProjectionKind.Place, restored.Kind);
|
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
|
.AcknowledgeProjection(restored.Token));
|
|
Assert.True(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence(
|
|
token,
|
|
successorGeneration: 1UL,
|
|
successorReady: true));
|
|
Assert.Equal(restored.Token.ExactCellId, record.FullCellId);
|
|
Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(record));
|
|
}
|
|
|
|
private sealed class Fixture : IDisposable
|
|
{
|
|
private readonly RuntimeGenerationToken _generation = new(7UL);
|
|
|
|
internal Fixture(
|
|
bool bindGeneration = false,
|
|
PhysicsEngine? engine = null)
|
|
{
|
|
Lifetime = new RuntimeEntityObjectLifetime(engine ?? FlatEngine());
|
|
if (bindGeneration)
|
|
{
|
|
Lifetime.BindEventContext(
|
|
() => _generation,
|
|
static () => 11UL);
|
|
}
|
|
}
|
|
|
|
internal RuntimeEntityObjectLifetime Lifetime { get; }
|
|
|
|
internal RuntimeEntityRecord Add(
|
|
uint guid,
|
|
ushort incarnation,
|
|
uint cell,
|
|
Vector3 position)
|
|
{
|
|
RuntimeEntityRecord record = Lifetime.RegisterEntity(
|
|
Spawn(guid, incarnation, cell, position)).Canonical!;
|
|
Lifetime.Entities.SetFinalPhysicsState(
|
|
record,
|
|
PhysicsStateFlags.Gravity);
|
|
Lifetime.Entities.SetFullCell(
|
|
record,
|
|
cell,
|
|
(cell & 0xFFFF0000u) | 0xFFFFu);
|
|
var body = new PhysicsBody
|
|
{
|
|
Position = position,
|
|
Orientation = Quaternion.Identity,
|
|
LastUpdateTime = 1d,
|
|
State = PhysicsStateFlags.Gravity,
|
|
TransientState = TransientStateFlags.Active,
|
|
};
|
|
body.SnapToCell(cell, position, position);
|
|
Lifetime.Entities.SetPhysicsBody(record, body);
|
|
record.ObjectClock.Activate();
|
|
Lifetime.Physics.AcknowledgeSpatialProjection(record, spatial: true);
|
|
return record;
|
|
}
|
|
|
|
internal RuntimeSetPositionOutcome Place(
|
|
RuntimeEntityRecord record,
|
|
uint cell,
|
|
Vector3 position,
|
|
uint? currentCell = null) => Lifetime.Physics.SetPosition.Apply(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
Command(cell, position, currentCell));
|
|
|
|
internal RuntimeCollisionPrefixQuiescenceToken Begin(
|
|
ulong generation) => Lifetime.Physics
|
|
.BeginCollisionPrefixQuiescence(
|
|
PrefixP,
|
|
generation,
|
|
includeOutdoorCells: true);
|
|
|
|
internal RuntimeCollisionPrefixQuiescenceToken Begin(
|
|
uint prefix,
|
|
ulong generation,
|
|
bool includeOutdoorCells) => Lifetime.Physics
|
|
.BeginCollisionPrefixQuiescence(
|
|
prefix,
|
|
generation,
|
|
includeOutdoorCells);
|
|
|
|
internal bool TryAcquire(
|
|
RuntimeCollisionPrefixQuiescenceToken token,
|
|
out RuntimeCollisionPrefixMutationPermission permission) =>
|
|
Lifetime.Physics.TryAcquireCollisionPrefixMutationPermission(
|
|
token,
|
|
out permission);
|
|
|
|
internal RuntimePlacementProjectionSubscription Subscribe(
|
|
IRuntimePlacementProjectionSink sink) => new(
|
|
Lifetime.Placements,
|
|
() => _generation,
|
|
sink,
|
|
retryPendingOnSubscribe: true);
|
|
|
|
public void Dispose() => Lifetime.Dispose();
|
|
}
|
|
|
|
private sealed class RecordingSink(
|
|
Func<RuntimePlacementProjectionSnapshot, bool> apply)
|
|
: IRuntimePlacementProjectionSink
|
|
{
|
|
internal List<RuntimePlacementProjectionSnapshot> Applied { get; } = [];
|
|
|
|
public bool TryApply(in RuntimePlacementProjectionSnapshot projection)
|
|
{
|
|
Applied.Add(projection);
|
|
return apply(projection);
|
|
}
|
|
}
|
|
|
|
private sealed class PlacementObserver(
|
|
Action<RuntimePlacementDelta>? onPlacement = null)
|
|
: IRuntimePlacementObserver
|
|
{
|
|
public void OnPlacement(in RuntimePlacementDelta delta) =>
|
|
onPlacement?.Invoke(delta);
|
|
}
|
|
|
|
private static RuntimeSetPositionCommand Command(
|
|
uint cell,
|
|
Vector3 position,
|
|
uint? currentCell) => new(
|
|
new PhysicsSetPositionRequest(
|
|
position,
|
|
Quaternion.Identity,
|
|
cell,
|
|
position,
|
|
ImmutableArray<FlatCollisionSphere>.Empty,
|
|
Scale: 1f,
|
|
StepUpHeight: 0.4f,
|
|
StepDownHeight: 0.4f,
|
|
Flags: PhysicsSetPositionFlags.Placement
|
|
| PhysicsSetPositionFlags.Slide,
|
|
CurrentCellId: currentCell),
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative,
|
|
GameTime: 10d,
|
|
ExpectedVelocityAuthorityVersion: 0UL);
|
|
|
|
private static RuntimeSetPositionCommand PrepareAuthoredCommand(
|
|
RuntimeEntityObjectLifetime lifetime,
|
|
RuntimeEntityPlacementToken token)
|
|
{
|
|
var setup = new FlatSetupCollision(
|
|
ImmutableArray<FlatCollisionCylinder>.Empty,
|
|
[new FlatCollisionSphere(Vector3.Zero, 0.4f)],
|
|
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 PhysicsEngine FlatEngine()
|
|
{
|
|
var engine = new PhysicsEngine
|
|
{
|
|
DataCache = new PhysicsDataCache(),
|
|
};
|
|
AddFlatLandblock(engine, PrefixP);
|
|
AddFlatLandblock(engine, PrefixQ);
|
|
return engine;
|
|
}
|
|
|
|
private static void AddFlatLandblock(PhysicsEngine engine, uint prefix) =>
|
|
engine.AddLandblock(
|
|
prefix,
|
|
new TerrainSurface(new byte[81], new float[256]),
|
|
Array.Empty<CellSurface>(),
|
|
Array.Empty<PortalPlane>(),
|
|
worldOffsetX: 0f,
|
|
worldOffsetY: 0f);
|
|
|
|
private static CellPhysics ContainmentCell(
|
|
Plane plane,
|
|
uint[] visibleCells) => new()
|
|
{
|
|
BSP = new PhysicsBSPTree
|
|
{
|
|
Root = new PhysicsBSPNode { Type = BSPNodeType.Leaf },
|
|
},
|
|
WorldTransform = Matrix4x4.Identity,
|
|
InverseWorldTransform = Matrix4x4.Identity,
|
|
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
|
|
CellBSP = new CellBSPTree
|
|
{
|
|
Root = new CellBSPNode
|
|
{
|
|
SplittingPlane = plane,
|
|
PosNode = new CellBSPNode { Type = BSPNodeType.Leaf },
|
|
},
|
|
},
|
|
Portals = [new PortalInfo(0xFFFF, 0, 0)],
|
|
PortalPolygons = new Dictionary<ushort, ResolvedPolygon>(),
|
|
VisibleCellIds = new HashSet<uint>(visibleCells),
|
|
};
|
|
|
|
private static WorldSession.EntitySpawn Spawn(
|
|
uint guid,
|
|
ushort instance,
|
|
uint cell,
|
|
Vector3 position)
|
|
{
|
|
var serverPosition = new CreateObject.ServerPosition(
|
|
cell,
|
|
position.X,
|
|
position.Y,
|
|
position.Z,
|
|
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: serverPosition,
|
|
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,
|
|
serverPosition,
|
|
0x02000001u,
|
|
Array.Empty<CreateObject.AnimPartChange>(),
|
|
Array.Empty<CreateObject.TextureChange>(),
|
|
Array.Empty<CreateObject.SubPaletteSwap>(),
|
|
null,
|
|
null,
|
|
"collision-prefix-quiescence-fixture",
|
|
null,
|
|
null,
|
|
0x09000001u,
|
|
PhysicsState: (uint)PhysicsStateFlags.Gravity,
|
|
InstanceSequence: instance,
|
|
MovementSequence: 1,
|
|
ServerControlSequence: 1,
|
|
PositionSequence: 1,
|
|
Physics: physics);
|
|
}
|
|
}
|