feat(runtime): commit dormant SetPosition activation
This commit is contained in:
parent
99f867f053
commit
5785a07b3e
13 changed files with 4674 additions and 111 deletions
|
|
@ -2,19 +2,20 @@
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
This is placement Slice 4B2 checkpoints 4-5. It adds the dormant,
|
This is placement Slice 4B2 checkpoints 4-6. It adds the dormant,
|
||||||
presentation-independent transaction which prepares and assigns ownership of
|
presentation-independent transaction which prepares and assigns ownership of
|
||||||
one local-player `PhysicsBody` and `PlayerMovementController`, then retains an
|
one local-player `PhysicsBody` and `PlayerMovementController`, retains an exact
|
||||||
exact post-ownership activation lease which can evaluate retail SetPosition
|
post-ownership evaluation lease, and commits the canonical Runtime SetPosition
|
||||||
without publishing it. No App or
|
activation in retail order. No App or Headless production route invokes this
|
||||||
Headless production route invokes this transaction, so graphical and no-window
|
transaction yet, so graphical and no-window game behavior is unchanged and
|
||||||
game behavior is unchanged and AP-1/AD-1 remain open.
|
AP-1/AD-1 remain open until their hosts cut over.
|
||||||
|
|
||||||
The checkpoints deliberately stop before canonical SetPosition activation.
|
Checkpoint 6 consumes the prepared placement operation only after the exact
|
||||||
They do not consume the prepared placement operation, enter the body into the
|
body/controller/identity/collision envelope is current. It publishes FullCell,
|
||||||
physics engine, publish FullCell/world/host/shadow/workset state, or project a
|
world residence, host, shadow, workset, object-clock, and ordered Place state
|
||||||
presentation entity. Those effects belong to the next transaction and must all
|
from that same Runtime-owned dormant body. There is no second body, mirrored
|
||||||
use the same Runtime-owned dormant body.
|
gameplay owner, rollback mutation, or presentation callback inside the
|
||||||
|
canonical tail.
|
||||||
|
|
||||||
## Ownership contract
|
## Ownership contract
|
||||||
|
|
||||||
|
|
@ -46,8 +47,9 @@ the same server GUID as the exact entity incarnation.
|
||||||
Unpublished candidates and ownership-committed dormant controllers reject live
|
Unpublished candidates and ownership-committed dormant controllers reject live
|
||||||
movement operations: update, public SetPosition, blip, outbound-position
|
movement operations: update, public SetPosition, blip, outbound-position
|
||||||
capture, movement/position send tracking, and shared-engine position commit.
|
capture, movement/position send tracking, and shared-engine position commit.
|
||||||
Only the subsequent activation transaction may promote `RuntimeOwnedDormant`
|
Only the checkpoint-6 activation transaction may promote `RuntimeOwnedDormant`
|
||||||
to `RuntimePublished`; this checkpoint never invokes that transition. Once a
|
to `RuntimePublished`; preparation and evaluation never invoke that transition.
|
||||||
|
Once a
|
||||||
Runtime-owned dormant or published controller is replaced, reset, or disposed,
|
Runtime-owned dormant or published controller is replaced, reset, or disposed,
|
||||||
its terminal retirement state rejects the same operations plus
|
its terminal retirement state rejects the same operations plus
|
||||||
body/configuration mutation and manager acquisition. Publicly constructed
|
body/configuration mutation and manager acquisition. Publicly constructed
|
||||||
|
|
@ -106,8 +108,9 @@ transaction is pure: it returns committed, deferred-cell, or rejected
|
||||||
placement data without writing the canonical body, FullCell, clock, spatial
|
placement data without writing the canonical body, FullCell, clock, spatial
|
||||||
worksets, shadows, collision-report owners, host, operation stage, or Place
|
worksets, shadows, collision-report owners, host, operation stage, or Place
|
||||||
projection. A missing cell therefore leaves the exact body dormant and the
|
projection. A missing cell therefore leaves the exact body dormant and the
|
||||||
authored operation retryable. A valid result likewise remains only an
|
authored operation retryable. During evaluation, a valid result likewise
|
||||||
immutable evaluation receipt; this checkpoint has no activation/commit API.
|
remains only an immutable receipt; checkpoint 6's separate commit API consumes
|
||||||
|
that receipt only after revalidating the complete activation envelope.
|
||||||
|
|
||||||
Each evaluation carries an append-only, stable-order union of every cell read
|
Each evaluation carries an append-only, stable-order union of every cell read
|
||||||
by the complete Core transaction: the AdjustPosition seed and adjusted cell,
|
by the complete Core transaction: the AdjustPosition seed and adjusted cell,
|
||||||
|
|
@ -154,6 +157,71 @@ discard is required before a new candidate can be prepared. Reset and disposal
|
||||||
retire the lease, body, and dormant controller and include the pending
|
retire the lease, body, and dormant controller and include the pending
|
||||||
activation in the ownership convergence ledger.
|
activation in the ownership convergence ledger.
|
||||||
|
|
||||||
|
## Canonical activation and retail ordering
|
||||||
|
|
||||||
|
The implementation follows the named-retail chain rather than treating
|
||||||
|
SetPosition as a single opaque callback:
|
||||||
|
|
||||||
|
- `CPhysicsObj::SetPosition` at `0x005160C0` owns the outer placement call.
|
||||||
|
- The internal wrapper at `0x00515BD0` evaluates residence and collision.
|
||||||
|
- `CPhysicsObj::SetPositionInternal(CTransition*)` at `0x00515330` commits the
|
||||||
|
accepted frame/contact prefix and later shadow/cell state.
|
||||||
|
- `CPhysicsObj::enter_world` at `0x00516170` is the final live edge.
|
||||||
|
- `CPhysicsObj::leave_world` at `0x005155A0` is the canonical retirement edge.
|
||||||
|
|
||||||
|
Runtime splits that chain into a prepared, callback-free transaction and an
|
||||||
|
ordered notification suffix:
|
||||||
|
|
||||||
|
1. Install the accepted frame and contact prefix on the still-dormant body and
|
||||||
|
perform the first acceleration calculation.
|
||||||
|
2. Open one narrow dormant ground phase and invoke `HitGround` or
|
||||||
|
`LeaveGround`. Movement reapplication may call retail `set_velocity`, but
|
||||||
|
the phase closes with `Active=false`; the body is still out of world, has no
|
||||||
|
host/spatial membership, and its object clock is inactive.
|
||||||
|
3. Synchronize accepted State and Vector authorities, run the post-ground
|
||||||
|
acceleration/sliding phase, and dispatch the already-installed collision
|
||||||
|
batch.
|
||||||
|
4. Revalidate the complete ownership/collision envelope. Accepted State and
|
||||||
|
Vector updates are synchronized; Position, ObjDesc, Create, Setup,
|
||||||
|
incarnation, identity, collision-generation, body, controller, host, or
|
||||||
|
session displacement aborts the old transaction.
|
||||||
|
5. Apply velocity-current physical response and stationary bits, prepare the
|
||||||
|
final shadow mutation and Place receipt, then perform the callback-free
|
||||||
|
FullCell/body/host/controller/spatial/object-clock tail.
|
||||||
|
6. Dispatch exact shadow notifications and the ordered Place projection only
|
||||||
|
after the complete live graph is visible.
|
||||||
|
|
||||||
|
Collision and shadow mutations use explicit prepare/apply/dispatch receipts.
|
||||||
|
Receipt dispatch is exact-once and owner-local, so reverse-order receipts for
|
||||||
|
different owners remain valid while a superseding mutation of the same owner
|
||||||
|
stops the stale suffix. Collision owner states carry the exact SetPosition
|
||||||
|
batch ID. Reentrant Position or newer-batch replacement suppresses remaining
|
||||||
|
reciprocal/environment callbacks, and abort cleanup force-ends/removes only the
|
||||||
|
still-exact old batch, including reverse rows and the environment latch. The
|
||||||
|
combined Runtime physics ownership ledger includes pending collision and
|
||||||
|
shadow SetPosition receipts; teardown cannot report convergence while either
|
||||||
|
receipt remains.
|
||||||
|
|
||||||
|
Candidate construction applies the accepted `PhysicsDesc` values in retail
|
||||||
|
`CPhysicsObj::set_description` order before sealing ownership: final state,
|
||||||
|
friction, clamped elasticity, `set_velocity` (including the 50-unit clamp),
|
||||||
|
and angular velocity. Network acceleration remains parse-only because retail
|
||||||
|
recalculates it from the final physics state. This initial vector bootstrap is
|
||||||
|
required even when the SetPosition receipt's source Vector authority is still
|
||||||
|
current; the later refresh intentionally skips in that case. Collision
|
||||||
|
callbacks may advance State/Vector authority without invalidating the
|
||||||
|
immutable geometry/identity envelope, and a changed Vector authority refreshes
|
||||||
|
the dormant body through the same `set_velocity` path before physical response.
|
||||||
|
|
||||||
|
A deferred-cell commit atomically suspends an authored shadow registration and
|
||||||
|
consumes its notification receipt. Explicit publication discard cancels the
|
||||||
|
exact SetPosition lease and body/controller ownership, while the suspended
|
||||||
|
registration remains owned by the live entity/shadow registry and is reusable
|
||||||
|
by a later activation. A deterministic discard -> generation-ready -> new
|
||||||
|
activation gate proves the same registration restores without stale rows or a
|
||||||
|
pending receipt. Entity/lifetime teardown remains the terminal owner of that
|
||||||
|
suspended registration.
|
||||||
|
|
||||||
## Gates
|
## Gates
|
||||||
|
|
||||||
- Candidate privacy and live-operation rejection.
|
- Candidate privacy and live-operation rejection.
|
||||||
|
|
@ -161,15 +229,22 @@ activation in the ownership convergence ledger.
|
||||||
acquisition/binding, and remote-placement ownership.
|
acquisition/binding, and remote-placement ownership.
|
||||||
- Exact local-player identity, identity-switch, and disposed-identity rejection.
|
- Exact local-player identity, identity-switch, and disposed-identity rejection.
|
||||||
- Exact same-body ownership in entity record and dormant movement controller.
|
- Exact same-body ownership in entity record and dormant movement controller.
|
||||||
- Dormant rejection after ownership commit plus the isolated controller-level
|
- Initial PhysicsDesc velocity, angular velocity, friction, and elasticity
|
||||||
`dormant -> activated -> live` lifecycle contract for the next checkpoint.
|
bootstrap, including activation with the retail 50-unit velocity clamp.
|
||||||
|
- Dormant rejection after ownership commit plus the controller-level
|
||||||
|
`dormant -> activated -> live` lifecycle contract exercised by checkpoint 6.
|
||||||
- No mutation of SetPosition, FullCell, spatial roots, host projections,
|
- No mutation of SetPosition, FullCell, spatial roots, host projections,
|
||||||
shadows, worksets, world residence, or presentation during this checkpoint.
|
shadows, worksets, world residence, or presentation during preparation or
|
||||||
|
evaluation; the separately gated activation commit owns those mutations.
|
||||||
- Replacement by position, vector, final physics state, object description,
|
- Replacement by position, vector, final physics state, object description,
|
||||||
CreateObject, remote/projectile/body/clock/controller ownership, and explicit
|
CreateObject, remote/projectile/body/clock/controller ownership, and explicit
|
||||||
placement cancellation.
|
placement cancellation.
|
||||||
- Delete plus same-GUID reincarnation.
|
- Delete plus same-GUID reincarnation.
|
||||||
- Candidate replacement, reset, disposal, and ownership convergence.
|
- Candidate replacement, reset, disposal, and ownership convergence.
|
||||||
|
- Publication/activation sequence exhaustion is preflighted before candidate
|
||||||
|
allocation or replacement, leaving no private or canonical owner behind.
|
||||||
|
- Shadow-registry reset invalidates even a prepared, unapplied shapeless
|
||||||
|
transaction which owns no logical rows or pending dispatch receipt.
|
||||||
- Pure committed/deferred/rejected SetPosition evaluation with bit-exact
|
- Pure committed/deferred/rejected SetPosition evaluation with bit-exact
|
||||||
body-state snapshots and no canonical, collision-report, projection,
|
body-state snapshots and no canonical, collision-report, projection,
|
||||||
clock, FullCell, host, shadow, workset, or operation-stage mutation.
|
clock, FullCell, host, shadow, workset, or operation-stage mutation.
|
||||||
|
|
@ -193,30 +268,20 @@ activation in the ownership convergence ledger.
|
||||||
- Terminal stale-controller rejection after replacement, reset, and disposal.
|
- Terminal stale-controller rejection after replacement, reset, and disposal.
|
||||||
- Body/controller epochs advance only on actual ownership changes.
|
- Body/controller epochs advance only on actual ownership changes.
|
||||||
|
|
||||||
The checkpoint-5 focused publication suite passes 70/70 and the focused Core
|
The checkpoint-6 focused publication/collision suite passes 129/129, the
|
||||||
SetPosition suite passes 59/59. The complete Runtime project passes 666/666;
|
focused Core shadow transaction suite passes 16/16, and the complete Runtime
|
||||||
the complete Core project passes 4,230 tests / 1 skip; and the App Runtime
|
project passes 695/695 under invariant globalization. The Runtime Release build
|
||||||
physics/movement ownership guard passes 6/6. The complete Release solution
|
passes with zero warnings and zero errors. Broader Core/App/solution and
|
||||||
builds with zero errors (three pre-existing App-test nullability warnings are
|
connected gates remain for the parent integration checkpoint. Under the
|
||||||
outside this checkpoint), and its invariant-culture test gate passes 10,419
|
machine's Swedish current culture, the three previously known formatting
|
||||||
tests / 4 intentional skips. The installed prepared-package gate
|
assertions remain unrelated (`0,5` versus `0.5` and localized sky text), so the
|
||||||
uses the exact local `acdream.pak`. Under the machine's Swedish current culture,
|
canonical Runtime gate runs under invariant globalization.
|
||||||
the same three previously known formatting assertions remain unrelated (`0,5`
|
|
||||||
versus `0.5` and localized sky text), so the canonical gate runs under
|
|
||||||
invariant culture.
|
|
||||||
|
|
||||||
## Next checkpoint
|
## Next checkpoint
|
||||||
|
|
||||||
Add the canonical Runtime SetPosition activation transaction. Before its
|
Cut the graphical and no-window local-player hosts over to this Runtime-owned
|
||||||
callback-free tail, it must prepare a presentation-independent Runtime
|
activation transaction, then delete their duplicate SetPosition
|
||||||
`PhysicsHost`/`PositionManager`/`MoveToManager` graph, the existing authoritative
|
activation/publication paths. The cutover must preserve the same exact body,
|
||||||
authored shadow payload (without rebuilding AP-22 shapes), and an exact staged
|
controller, shadow payload, deferred-cell lease, collision receipt ordering,
|
||||||
collision-report batch. Then it may commit the already-owned dormant body,
|
and graceful teardown proven here; no host may reconstruct or replay the
|
||||||
contact/water/walkable/response state, object clock, FullCell, exact shadow,
|
canonical transaction.
|
||||||
spatial/ordinary workset, host, and SetPosition operation versions; seal the
|
|
||||||
ordered Place receipt; and invoke the sole `ActivateRuntimePublication`
|
|
||||||
transition as the final tickable edge. Collision reports and the Place observer
|
|
||||||
publish only afterward and must tolerate delete/reset re-entry without replaying
|
|
||||||
physics. Every pre-commit failure leaves the dormant lease and authored
|
|
||||||
operation retryable or rejects them; no rollback mutation and no second body or
|
|
||||||
controller are allowed.
|
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,20 @@ public sealed class PhysicsBody
|
||||||
/// point unchanged for an in-range local.
|
/// point unchanged for an in-range local.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SnapToCell(uint cellId, Vector3 worldPos, Vector3 cellLocal)
|
public void SnapToCell(uint cellId, Vector3 worldPos, Vector3 cellLocal)
|
||||||
|
{
|
||||||
|
StageDormantCellFrame(cellId, worldPos, cellLocal);
|
||||||
|
InWorld = true; // retail: enter_world / set_cell assigns physics_obj->cell
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Installs the exact SetPosition cell/frame while the Runtime owner is
|
||||||
|
/// still dormant. This is the frame half of retail SetPositionInternal;
|
||||||
|
/// enter_world remains a later explicit publication suffix.
|
||||||
|
/// </summary>
|
||||||
|
public void StageDormantCellFrame(
|
||||||
|
uint cellId,
|
||||||
|
Vector3 worldPos,
|
||||||
|
Vector3 cellLocal)
|
||||||
{
|
{
|
||||||
_position = worldPos;
|
_position = worldPos;
|
||||||
uint cell = cellId;
|
uint cell = cellId;
|
||||||
|
|
@ -206,7 +220,6 @@ public sealed class PhysicsBody
|
||||||
if ((cellId & 0xFFFFu) is >= 1u and <= 0x40u)
|
if ((cellId & 0xFFFFu) is >= 1u and <= 0x40u)
|
||||||
LandDefs.AdjustToOutside(ref cell, ref local);
|
LandDefs.AdjustToOutside(ref cell, ref local);
|
||||||
CellPosition = new Position(cell, new CellFrame(local, Orientation));
|
CellPosition = new Position(cell, new CellFrame(local, Orientation));
|
||||||
InWorld = true; // retail: enter_world / set_cell assigns physics_obj->cell
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -128,27 +128,11 @@ public static class PhysicsObjUpdate
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(body);
|
ArgumentNullException.ThrowIfNull(body);
|
||||||
|
|
||||||
if (previousOnWalkable)
|
bool finalOnWalkable = CommitSetPositionContactPrefix(
|
||||||
body.TransientState |= TransientStateFlags.OnWalkable;
|
body,
|
||||||
else
|
inContact,
|
||||||
body.TransientState &= ~TransientStateFlags.OnWalkable;
|
onWalkable,
|
||||||
|
previousOnWalkable);
|
||||||
if (inContact)
|
|
||||||
body.TransientState |= TransientStateFlags.Contact;
|
|
||||||
else
|
|
||||||
body.TransientState &= ~TransientStateFlags.Contact;
|
|
||||||
body.calc_acceleration();
|
|
||||||
|
|
||||||
bool finalOnWalkable = inContact && onWalkable;
|
|
||||||
if (finalOnWalkable)
|
|
||||||
body.TransientState |= TransientStateFlags.OnWalkable;
|
|
||||||
else
|
|
||||||
body.TransientState &= ~TransientStateFlags.OnWalkable;
|
|
||||||
|
|
||||||
if (body.ContactPlaneIsWater)
|
|
||||||
body.TransientState |= TransientStateFlags.WaterContact;
|
|
||||||
else
|
|
||||||
body.TransientState &= ~TransientStateFlags.WaterContact;
|
|
||||||
|
|
||||||
if (!previousOnWalkable && finalOnWalkable)
|
if (!previousOnWalkable && finalOnWalkable)
|
||||||
{
|
{
|
||||||
|
|
@ -162,10 +146,44 @@ public static class PhysicsObjUpdate
|
||||||
if (isCurrent?.Invoke() == false)
|
if (isCurrent?.Invoke() == false)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
body.calc_acceleration();
|
CommitSetPositionPostGround(body);
|
||||||
return isCurrent?.Invoke() ?? true;
|
return isCurrent?.Invoke() ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool CommitSetPositionContactPrefix(
|
||||||
|
PhysicsBody body,
|
||||||
|
bool inContact,
|
||||||
|
bool onWalkable,
|
||||||
|
bool previousOnWalkable)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(body);
|
||||||
|
if (previousOnWalkable)
|
||||||
|
body.TransientState |= TransientStateFlags.OnWalkable;
|
||||||
|
else
|
||||||
|
body.TransientState &= ~TransientStateFlags.OnWalkable;
|
||||||
|
if (inContact)
|
||||||
|
body.TransientState |= TransientStateFlags.Contact;
|
||||||
|
else
|
||||||
|
body.TransientState &= ~TransientStateFlags.Contact;
|
||||||
|
body.calc_acceleration();
|
||||||
|
bool finalOnWalkable = inContact && onWalkable;
|
||||||
|
if (finalOnWalkable)
|
||||||
|
body.TransientState |= TransientStateFlags.OnWalkable;
|
||||||
|
else
|
||||||
|
body.TransientState &= ~TransientStateFlags.OnWalkable;
|
||||||
|
if (body.ContactPlaneIsWater)
|
||||||
|
body.TransientState |= TransientStateFlags.WaterContact;
|
||||||
|
else
|
||||||
|
body.TransientState &= ~TransientStateFlags.WaterContact;
|
||||||
|
return finalOnWalkable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CommitSetPositionPostGround(PhysicsBody body)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(body);
|
||||||
|
body.calc_acceleration();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// retail <c>handle_all_collisions</c> (0x00514780). Reflects or zeros the body's
|
/// retail <c>handle_all_collisions</c> (0x00514780). Reflects or zeros the body's
|
||||||
/// <see cref="PhysicsBody.Velocity"/> (retail m_velocityVector) based on
|
/// <see cref="PhysicsBody.Velocity"/> (retail m_velocityVector) based on
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,10 @@ public sealed class ShadowObjectRegistry
|
||||||
private readonly HashSet<uint> _prefixScratch = new();
|
private readonly HashSet<uint> _prefixScratch = new();
|
||||||
private readonly List<uint> _removedPrefixScratch = new();
|
private readonly List<uint> _removedPrefixScratch = new();
|
||||||
private ulong _mutationRevision;
|
private ulong _mutationRevision;
|
||||||
|
private ulong _nextPreparedSetPositionCommitId;
|
||||||
|
private ulong _lastAppliedSetPositionCommitId;
|
||||||
|
private readonly HashSet<ulong> _pendingSetPositionDispatches = [];
|
||||||
|
private long _setPositionDispatchFailureCount;
|
||||||
internal event Action<uint, ulong>? OwnerMutated;
|
internal event Action<uint, ulong>? OwnerMutated;
|
||||||
internal event Action<uint, uint>? OwnerPrefixMembershipChanged;
|
internal event Action<uint, uint>? OwnerPrefixMembershipChanged;
|
||||||
|
|
||||||
|
|
@ -783,6 +787,553 @@ public sealed class ShadowObjectRegistry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Immutable owner-local shadow transaction prepared before Runtime's
|
||||||
|
/// SetPosition publication tail. The prepared rows are built against an
|
||||||
|
/// isolated registry; applying them never re-runs the flood oracle.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed record PreparedSetPositionShadowCommit(
|
||||||
|
ulong CommitId,
|
||||||
|
uint EntityId,
|
||||||
|
ulong ExpectedMutationRevision,
|
||||||
|
ulong ExpectedOwnerVersion,
|
||||||
|
ulong FinalMutationRevision,
|
||||||
|
ulong FinalOwnerVersion,
|
||||||
|
bool ProvenShapeless,
|
||||||
|
PreparedShadowOwnerState? OwnerState,
|
||||||
|
PreparedShadowCellReplacement[] CellReplacements,
|
||||||
|
PreparedShadowPrefixReplacement[] PrefixReplacements,
|
||||||
|
HashSet<uint>? OwnerPrefixes,
|
||||||
|
uint[] ChangedPrefixes);
|
||||||
|
|
||||||
|
internal sealed record PreparedShadowCellReplacement(
|
||||||
|
uint CellId,
|
||||||
|
List<ShadowEntry> Entries);
|
||||||
|
|
||||||
|
internal sealed record PreparedShadowPrefixReplacement(
|
||||||
|
uint Prefix,
|
||||||
|
bool Remove,
|
||||||
|
List<uint>? Slots,
|
||||||
|
Dictionary<uint, int>? Indices,
|
||||||
|
Stack<int>? FreeSlots);
|
||||||
|
|
||||||
|
internal readonly record struct SetPositionShadowCommitReceipt(
|
||||||
|
ulong CommitId,
|
||||||
|
uint EntityId,
|
||||||
|
ulong OwnerVersion,
|
||||||
|
uint[] ChangedPrefixes,
|
||||||
|
bool Mutated)
|
||||||
|
{
|
||||||
|
internal bool IsValid => CommitId != 0UL && EntityId != 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepares the complete owner-row replacement without touching the active
|
||||||
|
/// collision world. A missing registration is accepted only when the
|
||||||
|
/// caller carries an explicit proven-shapeless disposition; absence alone
|
||||||
|
/// is not evidence because an authored BSP payload may still be pending.
|
||||||
|
/// </summary>
|
||||||
|
internal bool TryPrepareSetPosition(
|
||||||
|
uint entityId,
|
||||||
|
Vector3 worldPosition,
|
||||||
|
Quaternion worldRotation,
|
||||||
|
uint seedCellId,
|
||||||
|
float worldOffsetX,
|
||||||
|
float worldOffsetY,
|
||||||
|
PhysicsShadowCommitAction action,
|
||||||
|
System.Collections.Immutable.ImmutableArray<uint> crossCellIds,
|
||||||
|
bool provenShapeless,
|
||||||
|
bool suspendOwner,
|
||||||
|
out PreparedSetPositionShadowCommit? prepared)
|
||||||
|
{
|
||||||
|
prepared = null;
|
||||||
|
ulong expectedMutation = _mutationRevision;
|
||||||
|
ulong expectedOwner = GetOwnerVersion(entityId);
|
||||||
|
bool hasOwner = TryCaptureOwnerState(
|
||||||
|
entityId,
|
||||||
|
out PreparedShadowOwnerState? source);
|
||||||
|
if (!hasOwner)
|
||||||
|
{
|
||||||
|
if (!provenShapeless)
|
||||||
|
return false;
|
||||||
|
_pendingSetPositionDispatches.EnsureCapacity(
|
||||||
|
_pendingSetPositionDispatches.Count + 1);
|
||||||
|
prepared = new PreparedSetPositionShadowCommit(
|
||||||
|
checked(++_nextPreparedSetPositionCommitId),
|
||||||
|
entityId,
|
||||||
|
expectedMutation,
|
||||||
|
expectedOwner,
|
||||||
|
expectedMutation,
|
||||||
|
expectedOwner,
|
||||||
|
ProvenShapeless: true,
|
||||||
|
OwnerState: null,
|
||||||
|
CellReplacements: [],
|
||||||
|
PrefixReplacements: [],
|
||||||
|
OwnerPrefixes: null,
|
||||||
|
ChangedPrefixes: Array.Empty<uint>());
|
||||||
|
return _mutationRevision == expectedMutation
|
||||||
|
&& GetOwnerVersion(entityId) == expectedOwner
|
||||||
|
&& !HasLogicalOwner(entityId);
|
||||||
|
}
|
||||||
|
if (provenShapeless || source is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var staging = new ShadowObjectRegistry
|
||||||
|
{
|
||||||
|
DataCache = DataCache,
|
||||||
|
};
|
||||||
|
staging.InstallOwnerState(source);
|
||||||
|
if (suspendOwner)
|
||||||
|
{
|
||||||
|
if (!staging.Suspend(entityId))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
staging.CommitSetPosition(
|
||||||
|
entityId,
|
||||||
|
worldPosition,
|
||||||
|
worldRotation,
|
||||||
|
seedCellId,
|
||||||
|
worldOffsetX,
|
||||||
|
worldOffsetY,
|
||||||
|
action,
|
||||||
|
crossCellIds);
|
||||||
|
}
|
||||||
|
if (!staging.TryCaptureOwnerState(
|
||||||
|
entityId,
|
||||||
|
out PreparedShadowOwnerState? replacement)
|
||||||
|
|| replacement is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint[] changedPrefixes = CaptureChangedPrefixes(source, replacement);
|
||||||
|
PreparedShadowCellReplacement[] cellReplacements =
|
||||||
|
PrepareCellReplacements(entityId, source, replacement);
|
||||||
|
HashSet<uint> replacementPrefixes = CapturePrefixes(replacement);
|
||||||
|
PreparedShadowPrefixReplacement[] prefixReplacements =
|
||||||
|
PreparePrefixReplacements(
|
||||||
|
entityId,
|
||||||
|
CapturePrefixes(source),
|
||||||
|
replacementPrefixes,
|
||||||
|
changedPrefixes);
|
||||||
|
ulong finalMutation = checked(expectedMutation + 1UL);
|
||||||
|
ulong finalOwner = checked(expectedOwner + 1UL);
|
||||||
|
// Reserve dictionary capacity before the non-fallible publication
|
||||||
|
// suffix. Row/list payloads themselves were allocated in staging.
|
||||||
|
_cells.EnsureCapacity(_cells.Count + replacement.Rows.Count);
|
||||||
|
_entityToCells.EnsureCapacity(_entityToCells.Count + 1);
|
||||||
|
_entityReg.EnsureCapacity(_entityReg.Count + 1);
|
||||||
|
_entityShapes.EnsureCapacity(_entityShapes.Count + 1);
|
||||||
|
_suspendedEntityCells.EnsureCapacity(_suspendedEntityCells.Count + 1);
|
||||||
|
_withdrawnPrefixesByOwner.EnsureCapacity(
|
||||||
|
_withdrawnPrefixesByOwner.Count + 1);
|
||||||
|
_ownerVersions.EnsureCapacity(_ownerVersions.Count + 1);
|
||||||
|
_ownerPrefixes.EnsureCapacity(_ownerPrefixes.Count + 1);
|
||||||
|
_prefixOwnerSlots.EnsureCapacity(
|
||||||
|
_prefixOwnerSlots.Count + changedPrefixes.Length);
|
||||||
|
_prefixOwnerIndices.EnsureCapacity(
|
||||||
|
_prefixOwnerIndices.Count + changedPrefixes.Length);
|
||||||
|
_prefixFreeSlots.EnsureCapacity(
|
||||||
|
_prefixFreeSlots.Count + changedPrefixes.Length);
|
||||||
|
_suspendedEntities.EnsureCapacity(_suspendedEntities.Count + 1);
|
||||||
|
_pendingSetPositionDispatches.EnsureCapacity(
|
||||||
|
_pendingSetPositionDispatches.Count + 1);
|
||||||
|
|
||||||
|
prepared = new PreparedSetPositionShadowCommit(
|
||||||
|
checked(++_nextPreparedSetPositionCommitId),
|
||||||
|
entityId,
|
||||||
|
expectedMutation,
|
||||||
|
expectedOwner,
|
||||||
|
finalMutation,
|
||||||
|
finalOwner,
|
||||||
|
ProvenShapeless: false,
|
||||||
|
replacement,
|
||||||
|
cellReplacements,
|
||||||
|
prefixReplacements,
|
||||||
|
replacementPrefixes,
|
||||||
|
changedPrefixes);
|
||||||
|
return _mutationRevision == expectedMutation
|
||||||
|
&& GetOwnerVersion(entityId) == expectedOwner
|
||||||
|
&& HasLogicalOwner(entityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies a previously prepared owner-local row swap with callbacks
|
||||||
|
/// suppressed. Runtime dispatches the returned exact notification only
|
||||||
|
/// after the complete SetPosition state suffix is visible.
|
||||||
|
/// </summary>
|
||||||
|
internal bool TryApplySetPosition(
|
||||||
|
PreparedSetPositionShadowCommit prepared,
|
||||||
|
out SetPositionShadowCommitReceipt receipt)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(prepared);
|
||||||
|
receipt = default;
|
||||||
|
if (prepared.CommitId <= _lastAppliedSetPositionCommitId
|
||||||
|
|| _mutationRevision != prepared.ExpectedMutationRevision
|
||||||
|
|| GetOwnerVersion(prepared.EntityId)
|
||||||
|
!= prepared.ExpectedOwnerVersion
|
||||||
|
|| HasLogicalOwner(prepared.EntityId)
|
||||||
|
== prepared.ProvenShapeless)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prepared.ProvenShapeless)
|
||||||
|
{
|
||||||
|
receipt = new SetPositionShadowCommitReceipt(
|
||||||
|
prepared.CommitId,
|
||||||
|
prepared.EntityId,
|
||||||
|
prepared.ExpectedOwnerVersion,
|
||||||
|
Array.Empty<uint>(),
|
||||||
|
Mutated: false);
|
||||||
|
_lastAppliedSetPositionCommitId = prepared.CommitId;
|
||||||
|
_pendingSetPositionDispatches.Add(prepared.CommitId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (prepared.OwnerState is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (int index = 0; index < prepared.CellReplacements.Length; index++)
|
||||||
|
{
|
||||||
|
PreparedShadowCellReplacement replacement =
|
||||||
|
prepared.CellReplacements[index];
|
||||||
|
_cells[replacement.CellId] = replacement.Entries;
|
||||||
|
}
|
||||||
|
PreparedShadowOwnerState state = prepared.OwnerState;
|
||||||
|
_entityReg[prepared.EntityId] = state.Registration;
|
||||||
|
ReplaceOwnerValue(_entityShapes, prepared.EntityId, state.Shapes);
|
||||||
|
if (state.Suspended)
|
||||||
|
_suspendedEntities.Add(prepared.EntityId);
|
||||||
|
else
|
||||||
|
_suspendedEntities.Remove(prepared.EntityId);
|
||||||
|
ReplaceOwnerValue(
|
||||||
|
_suspendedEntityCells,
|
||||||
|
prepared.EntityId,
|
||||||
|
state.SuspendedCellIds);
|
||||||
|
ReplaceOwnerValue(
|
||||||
|
_withdrawnPrefixesByOwner,
|
||||||
|
prepared.EntityId,
|
||||||
|
state.WithdrawnPrefixes);
|
||||||
|
ReplaceOwnerValue(
|
||||||
|
_entityToCells,
|
||||||
|
prepared.EntityId,
|
||||||
|
state.CellIds);
|
||||||
|
if (prepared.OwnerPrefixes is not null)
|
||||||
|
_ownerPrefixes[prepared.EntityId] = prepared.OwnerPrefixes;
|
||||||
|
for (int index = 0; index < prepared.PrefixReplacements.Length; index++)
|
||||||
|
{
|
||||||
|
PreparedShadowPrefixReplacement replacement =
|
||||||
|
prepared.PrefixReplacements[index];
|
||||||
|
if (replacement.Remove)
|
||||||
|
{
|
||||||
|
_prefixOwnerSlots.Remove(replacement.Prefix);
|
||||||
|
_prefixOwnerIndices.Remove(replacement.Prefix);
|
||||||
|
_prefixFreeSlots.Remove(replacement.Prefix);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_prefixOwnerSlots[replacement.Prefix] = replacement.Slots!;
|
||||||
|
_prefixOwnerIndices[replacement.Prefix] = replacement.Indices!;
|
||||||
|
_prefixFreeSlots[replacement.Prefix] = replacement.FreeSlots!;
|
||||||
|
}
|
||||||
|
_mutationRevision = prepared.FinalMutationRevision;
|
||||||
|
_ownerVersions[prepared.EntityId] = prepared.FinalOwnerVersion;
|
||||||
|
_lastAppliedSetPositionCommitId = prepared.CommitId;
|
||||||
|
_pendingSetPositionDispatches.Add(prepared.CommitId);
|
||||||
|
receipt = new SetPositionShadowCommitReceipt(
|
||||||
|
prepared.CommitId,
|
||||||
|
prepared.EntityId,
|
||||||
|
prepared.FinalOwnerVersion,
|
||||||
|
prepared.ChangedPrefixes,
|
||||||
|
Mutated: true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool IsPreparedSetPositionCurrent(
|
||||||
|
PreparedSetPositionShadowCommit prepared)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(prepared);
|
||||||
|
return prepared.CommitId > _lastAppliedSetPositionCommitId
|
||||||
|
&& _mutationRevision == prepared.ExpectedMutationRevision
|
||||||
|
&& GetOwnerVersion(prepared.EntityId)
|
||||||
|
== prepared.ExpectedOwnerVersion
|
||||||
|
&& HasLogicalOwner(prepared.EntityId)
|
||||||
|
!= prepared.ProvenShapeless;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void DispatchSetPositionCommit(
|
||||||
|
in SetPositionShadowCommitReceipt receipt)
|
||||||
|
{
|
||||||
|
if (!receipt.IsValid
|
||||||
|
|| receipt.CommitId > _lastAppliedSetPositionCommitId
|
||||||
|
|| !_pendingSetPositionDispatches.Remove(receipt.CommitId))
|
||||||
|
return;
|
||||||
|
if (!receipt.Mutated)
|
||||||
|
return;
|
||||||
|
ulong currentOwnerVersion = GetOwnerVersion(receipt.EntityId);
|
||||||
|
if (!HasLogicalOwner(receipt.EntityId)
|
||||||
|
|| currentOwnerVersion != receipt.OwnerVersion)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int index = 0; index < receipt.ChangedPrefixes.Length; index++)
|
||||||
|
{
|
||||||
|
if (!HasLogicalOwner(receipt.EntityId)
|
||||||
|
|| GetOwnerVersion(receipt.EntityId) != receipt.OwnerVersion)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DispatchSetPositionPrefixObservers(
|
||||||
|
receipt.EntityId,
|
||||||
|
receipt.ChangedPrefixes[index]);
|
||||||
|
}
|
||||||
|
if (!HasLogicalOwner(receipt.EntityId))
|
||||||
|
return;
|
||||||
|
currentOwnerVersion = GetOwnerVersion(receipt.EntityId);
|
||||||
|
if (currentOwnerVersion != receipt.OwnerVersion)
|
||||||
|
return;
|
||||||
|
DispatchSetPositionOwnerObservers(
|
||||||
|
receipt.EntityId,
|
||||||
|
currentOwnerVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool DiscardSetPositionCommit(
|
||||||
|
in SetPositionShadowCommitReceipt receipt) =>
|
||||||
|
receipt.IsValid
|
||||||
|
&& _pendingSetPositionDispatches.Remove(receipt.CommitId);
|
||||||
|
|
||||||
|
internal int PendingSetPositionDispatchCount =>
|
||||||
|
_pendingSetPositionDispatches.Count;
|
||||||
|
|
||||||
|
internal long SetPositionDispatchFailureCount =>
|
||||||
|
_setPositionDispatchFailureCount;
|
||||||
|
|
||||||
|
private void DispatchSetPositionPrefixObservers(uint owner, uint prefix)
|
||||||
|
{
|
||||||
|
Action<uint, uint>? observers = OwnerPrefixMembershipChanged;
|
||||||
|
if (observers is null)
|
||||||
|
return;
|
||||||
|
foreach (Action<uint, uint> observer in observers.GetInvocationList())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
observer(owner, prefix);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_setPositionDispatchFailureCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DispatchSetPositionOwnerObservers(uint owner, ulong version)
|
||||||
|
{
|
||||||
|
Action<uint, ulong>? observers = OwnerMutated;
|
||||||
|
if (observers is null)
|
||||||
|
return;
|
||||||
|
foreach (Action<uint, ulong> observer in observers.GetInvocationList())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
observer(owner, version);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_setPositionDispatchFailureCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint[] CaptureChangedPrefixes(
|
||||||
|
PreparedShadowOwnerState before,
|
||||||
|
PreparedShadowOwnerState after)
|
||||||
|
{
|
||||||
|
HashSet<uint> oldPrefixes = CapturePrefixes(before);
|
||||||
|
HashSet<uint> newPrefixes = CapturePrefixes(after);
|
||||||
|
var changed = new List<uint>();
|
||||||
|
foreach (uint prefix in oldPrefixes)
|
||||||
|
{
|
||||||
|
if (!newPrefixes.Contains(prefix))
|
||||||
|
changed.Add(prefix);
|
||||||
|
}
|
||||||
|
foreach (uint prefix in newPrefixes)
|
||||||
|
{
|
||||||
|
if (!oldPrefixes.Contains(prefix))
|
||||||
|
changed.Add(prefix);
|
||||||
|
}
|
||||||
|
changed.Sort();
|
||||||
|
return changed.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HashSet<uint> CapturePrefixes(
|
||||||
|
PreparedShadowOwnerState state)
|
||||||
|
{
|
||||||
|
var prefixes = new HashSet<uint>
|
||||||
|
{
|
||||||
|
state.Registration.SeedCellId & 0xFFFF0000u,
|
||||||
|
};
|
||||||
|
if (state.CellIds is not null)
|
||||||
|
{
|
||||||
|
for (int index = 0; index < state.CellIds.Count; index++)
|
||||||
|
prefixes.Add(state.CellIds[index] & 0xFFFF0000u);
|
||||||
|
}
|
||||||
|
if (state.WithdrawnPrefixes is not null)
|
||||||
|
{
|
||||||
|
foreach (uint prefix in state.WithdrawnPrefixes)
|
||||||
|
prefixes.Add(prefix & 0xFFFF0000u);
|
||||||
|
}
|
||||||
|
return prefixes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PreparedShadowCellReplacement[] PrepareCellReplacements(
|
||||||
|
uint entityId,
|
||||||
|
PreparedShadowOwnerState before,
|
||||||
|
PreparedShadowOwnerState after)
|
||||||
|
{
|
||||||
|
var touched = new HashSet<uint>();
|
||||||
|
AddCells(touched, before.CellIds);
|
||||||
|
AddCells(touched, after.CellIds);
|
||||||
|
var afterRows = new Dictionary<uint, ShadowEntry[]>();
|
||||||
|
for (int index = 0; index < after.Rows.Count; index++)
|
||||||
|
{
|
||||||
|
PreparedShadowCellRows row = after.Rows[index];
|
||||||
|
touched.Add(row.CellId);
|
||||||
|
afterRows[row.CellId] = row.Entries;
|
||||||
|
}
|
||||||
|
for (int index = 0; index < before.Rows.Count; index++)
|
||||||
|
touched.Add(before.Rows[index].CellId);
|
||||||
|
|
||||||
|
uint[] ordered = touched.ToArray();
|
||||||
|
Array.Sort(ordered);
|
||||||
|
var result = new PreparedShadowCellReplacement[ordered.Length];
|
||||||
|
for (int index = 0; index < ordered.Length; index++)
|
||||||
|
{
|
||||||
|
uint cellId = ordered[index];
|
||||||
|
_cells.TryGetValue(cellId, out List<ShadowEntry>? active);
|
||||||
|
afterRows.TryGetValue(cellId, out ShadowEntry[]? ownerRows);
|
||||||
|
int retainedCount = 0;
|
||||||
|
if (active is not null)
|
||||||
|
{
|
||||||
|
for (int row = 0; row < active.Count; row++)
|
||||||
|
{
|
||||||
|
if (active[row].EntityId != entityId)
|
||||||
|
retainedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var replacement = new List<ShadowEntry>(
|
||||||
|
retainedCount + (ownerRows?.Length ?? 0));
|
||||||
|
if (active is not null)
|
||||||
|
{
|
||||||
|
for (int row = 0; row < active.Count; row++)
|
||||||
|
{
|
||||||
|
if (active[row].EntityId != entityId)
|
||||||
|
replacement.Add(active[row]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ownerRows is not null)
|
||||||
|
replacement.AddRange(ownerRows);
|
||||||
|
result[index] = new PreparedShadowCellReplacement(
|
||||||
|
cellId,
|
||||||
|
replacement);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PreparedShadowPrefixReplacement[] PreparePrefixReplacements(
|
||||||
|
uint entityId,
|
||||||
|
HashSet<uint> before,
|
||||||
|
HashSet<uint> after,
|
||||||
|
uint[] changedPrefixes)
|
||||||
|
{
|
||||||
|
var result = new PreparedShadowPrefixReplacement[
|
||||||
|
changedPrefixes.Length];
|
||||||
|
for (int index = 0; index < changedPrefixes.Length; index++)
|
||||||
|
{
|
||||||
|
uint prefix = changedPrefixes[index];
|
||||||
|
bool removeOwner = before.Contains(prefix)
|
||||||
|
&& !after.Contains(prefix);
|
||||||
|
_prefixOwnerSlots.TryGetValue(prefix, out List<uint>? oldSlots);
|
||||||
|
_prefixOwnerIndices.TryGetValue(
|
||||||
|
prefix,
|
||||||
|
out Dictionary<uint, int>? oldIndices);
|
||||||
|
_prefixFreeSlots.TryGetValue(prefix, out Stack<int>? oldFree);
|
||||||
|
var slots = oldSlots is null ? [] : new List<uint>(oldSlots);
|
||||||
|
var indices = oldIndices is null
|
||||||
|
? new Dictionary<uint, int>()
|
||||||
|
: new Dictionary<uint, int>(oldIndices);
|
||||||
|
Stack<int> free = CloneStack(oldFree);
|
||||||
|
if (removeOwner)
|
||||||
|
{
|
||||||
|
if (indices.Remove(entityId, out int ownerSlot))
|
||||||
|
{
|
||||||
|
slots[ownerSlot] = 0u;
|
||||||
|
free.Push(ownerSlot);
|
||||||
|
}
|
||||||
|
result[index] = indices.Count == 0
|
||||||
|
? new PreparedShadowPrefixReplacement(
|
||||||
|
prefix,
|
||||||
|
Remove: true,
|
||||||
|
Slots: null,
|
||||||
|
Indices: null,
|
||||||
|
FreeSlots: null)
|
||||||
|
: new PreparedShadowPrefixReplacement(
|
||||||
|
prefix,
|
||||||
|
Remove: false,
|
||||||
|
slots,
|
||||||
|
indices,
|
||||||
|
free);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!indices.ContainsKey(entityId))
|
||||||
|
{
|
||||||
|
if (free.TryPop(out int freeIndex))
|
||||||
|
{
|
||||||
|
slots[freeIndex] = entityId;
|
||||||
|
indices[entityId] = freeIndex;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
indices[entityId] = slots.Count;
|
||||||
|
slots.Add(entityId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result[index] = new PreparedShadowPrefixReplacement(
|
||||||
|
prefix,
|
||||||
|
Remove: false,
|
||||||
|
slots,
|
||||||
|
indices,
|
||||||
|
free);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stack<int> CloneStack(Stack<int>? source) =>
|
||||||
|
source is null
|
||||||
|
? new Stack<int>()
|
||||||
|
: new Stack<int>(source.Reverse());
|
||||||
|
|
||||||
|
private static void AddCells(HashSet<uint> destination, List<uint>? cells)
|
||||||
|
{
|
||||||
|
if (cells is null)
|
||||||
|
return;
|
||||||
|
for (int index = 0; index < cells.Count; index++)
|
||||||
|
destination.Add(cells[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReplaceOwnerValue<T>(
|
||||||
|
Dictionary<uint, T> destination,
|
||||||
|
uint entityId,
|
||||||
|
T? value)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
destination.Remove(entityId);
|
||||||
|
else
|
||||||
|
destination[entityId] = value;
|
||||||
|
}
|
||||||
|
|
||||||
private void RefreshPositionRows(
|
private void RefreshPositionRows(
|
||||||
uint entityId,
|
uint entityId,
|
||||||
RegistrationRecord registration,
|
RegistrationRecord registration,
|
||||||
|
|
@ -1896,7 +2447,9 @@ public sealed class ShadowObjectRegistry
|
||||||
|| _entityToCells.Count != 0
|
|| _entityToCells.Count != 0
|
||||||
|| _entityReg.Count != 0
|
|| _entityReg.Count != 0
|
||||||
|| _suspendedEntities.Count != 0
|
|| _suspendedEntities.Count != 0
|
||||||
|| _suspendedEntityCells.Count != 0;
|
|| _suspendedEntityCells.Count != 0
|
||||||
|
|| _nextPreparedSetPositionCommitId
|
||||||
|
!= _lastAppliedSetPositionCommitId;
|
||||||
if (mutated)
|
if (mutated)
|
||||||
AdvanceMutationRevision();
|
AdvanceMutationRevision();
|
||||||
_cells.Clear();
|
_cells.Clear();
|
||||||
|
|
@ -1916,6 +2469,7 @@ public sealed class ShadowObjectRegistry
|
||||||
_ownerFreeSlots.Clear();
|
_ownerFreeSlots.Clear();
|
||||||
_prefixScratch.Clear();
|
_prefixScratch.Clear();
|
||||||
_removedPrefixScratch.Clear();
|
_removedPrefixScratch.Clear();
|
||||||
|
_pendingSetPositionDispatches.Clear();
|
||||||
_fallback = null;
|
_fallback = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -707,6 +707,57 @@ public sealed class PlayerMovementController
|
||||||
internal bool IsRuntimeOwnedDormant => _publicationLifecycle
|
internal bool IsRuntimeOwnedDormant => _publicationLifecycle
|
||||||
is PlayerMovementControllerPublicationLifecycle.RuntimeOwnedDormant;
|
is PlayerMovementControllerPublicationLifecycle.RuntimeOwnedDormant;
|
||||||
|
|
||||||
|
internal bool IsRuntimePublished => _publicationLifecycle
|
||||||
|
is PlayerMovementControllerPublicationLifecycle.RuntimePublished;
|
||||||
|
|
||||||
|
private bool _dormantSetPositionGroundPhase;
|
||||||
|
|
||||||
|
internal void BeginDormantSetPositionGroundPhase()
|
||||||
|
{
|
||||||
|
if (!IsRuntimeOwnedDormant || _dormantSetPositionGroundPhase)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Dormant SetPosition ground phase requires one dormant Runtime owner.");
|
||||||
|
_dormantSetPositionGroundPhase = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void EndDormantSetPositionGroundPhase()
|
||||||
|
{
|
||||||
|
if (!_dormantSetPositionGroundPhase)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Dormant SetPosition ground phase is not active.");
|
||||||
|
_body.TransientState &= ~TransientStateFlags.Active;
|
||||||
|
_dormantSetPositionGroundPhase = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool IsDormantSetPositionGroundPhaseActive =>
|
||||||
|
_dormantSetPositionGroundPhase;
|
||||||
|
|
||||||
|
internal void RefreshDormantRuntimePhysicsState(
|
||||||
|
PhysicsStateFlags state,
|
||||||
|
bool recalculateAcceleration)
|
||||||
|
{
|
||||||
|
if (!IsRuntimeOwnedDormant || _dormantSetPositionGroundPhase)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Only an idle dormant Runtime owner can refresh physics state.");
|
||||||
|
_body.State = state;
|
||||||
|
if (recalculateAcceleration)
|
||||||
|
_body.calc_acceleration();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void RefreshDormantRuntimeVector(
|
||||||
|
Vector3? velocity,
|
||||||
|
Vector3? omega)
|
||||||
|
{
|
||||||
|
if (!IsRuntimeOwnedDormant || _dormantSetPositionGroundPhase)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Only an idle dormant Runtime owner can refresh vector state.");
|
||||||
|
if (velocity is { } liveVelocity)
|
||||||
|
_body.set_velocity(liveVelocity);
|
||||||
|
if (omega is { } liveOmega)
|
||||||
|
_body.Omega = liveOmega;
|
||||||
|
_body.TransientState &= ~TransientStateFlags.Active;
|
||||||
|
}
|
||||||
|
|
||||||
internal void SealPublicationCandidate()
|
internal void SealPublicationCandidate()
|
||||||
{
|
{
|
||||||
if (_publicationLifecycle
|
if (_publicationLifecycle
|
||||||
|
|
@ -738,7 +789,8 @@ public sealed class PlayerMovementController
|
||||||
{
|
{
|
||||||
if (_publicationLifecycle
|
if (_publicationLifecycle
|
||||||
is not PlayerMovementControllerPublicationLifecycle
|
is not PlayerMovementControllerPublicationLifecycle
|
||||||
.RuntimeOwnedDormant)
|
.RuntimeOwnedDormant
|
||||||
|
|| _dormantSetPositionGroundPhase)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"Only a dormant Runtime-owned movement controller can be activated.");
|
"Only a dormant Runtime-owned movement controller can be activated.");
|
||||||
|
|
@ -747,6 +799,30 @@ public sealed class PlayerMovementController
|
||||||
.RuntimePublished;
|
.RuntimePublished;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Installs the already-committed Runtime SetPosition frame into the
|
||||||
|
/// controller's interpolation/cell sidecar without invoking the public
|
||||||
|
/// teleport path. The canonical body is written by Runtime first; this
|
||||||
|
/// method only makes the controller's private frame agree while it is
|
||||||
|
/// still dormant. It deliberately does not touch CellGraph, movement,
|
||||||
|
/// PositionManager, the object clock, or callbacks.
|
||||||
|
/// </summary>
|
||||||
|
internal void CommitRuntimeActivationFrame()
|
||||||
|
{
|
||||||
|
if (_publicationLifecycle
|
||||||
|
is not PlayerMovementControllerPublicationLifecycle
|
||||||
|
.RuntimeOwnedDormant
|
||||||
|
|| _dormantSetPositionGroundPhase)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Only a dormant Runtime-owned movement controller can accept its activation frame.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_prevPhysicsPos = _body.Position;
|
||||||
|
_currPhysicsPos = _body.Position;
|
||||||
|
CellId = _body.CellPosition.ObjCellId;
|
||||||
|
}
|
||||||
|
|
||||||
internal void DiscardRuntimeCandidate()
|
internal void DiscardRuntimeCandidate()
|
||||||
{
|
{
|
||||||
if (_publicationLifecycle
|
if (_publicationLifecycle
|
||||||
|
|
@ -775,7 +851,8 @@ public sealed class PlayerMovementController
|
||||||
is PlayerMovementControllerPublicationLifecycle.StandalonePublished
|
is PlayerMovementControllerPublicationLifecycle.StandalonePublished
|
||||||
or PlayerMovementControllerPublicationLifecycle
|
or PlayerMovementControllerPublicationLifecycle
|
||||||
.CandidatePreparing
|
.CandidatePreparing
|
||||||
or PlayerMovementControllerPublicationLifecycle.RuntimePublished)
|
or PlayerMovementControllerPublicationLifecycle.RuntimePublished
|
||||||
|
|| IsRuntimeOwnedDormant && _dormantSetPositionGroundPhase)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -807,6 +884,9 @@ public sealed class PlayerMovementController
|
||||||
if ((_body.State & PhysicsStateFlags.Static) != 0)
|
if ((_body.State & PhysicsStateFlags.Static) != 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (IsRuntimeOwnedDormant && _dormantSetPositionGroundPhase)
|
||||||
|
return;
|
||||||
|
|
||||||
_objectClock.Activate();
|
_objectClock.Activate();
|
||||||
_body.TransientState |= TransientStateFlags.Active;
|
_body.TransientState |= TransientStateFlags.Active;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
|
using AcDream.Core.Physics.Motion;
|
||||||
using AcDream.Runtime.Entities;
|
using AcDream.Runtime.Entities;
|
||||||
using AcDream.Runtime.Physics;
|
using AcDream.Runtime.Physics;
|
||||||
|
|
||||||
|
|
@ -23,6 +24,26 @@ internal enum RuntimeLocalPlayerPhysicsActivationStatus
|
||||||
RejectedToken,
|
RejectedToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal enum RuntimeLocalPlayerShadowDisposition : byte
|
||||||
|
{
|
||||||
|
RegisteredAuthoredPayload,
|
||||||
|
ProvenShapeless,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal readonly record struct RuntimeLocalPlayerPhysicsActivationPreparation(
|
||||||
|
float Radius,
|
||||||
|
float Height,
|
||||||
|
RuntimeLocalPlayerShadowDisposition ShadowDisposition)
|
||||||
|
{
|
||||||
|
internal bool IsValid => float.IsFinite(Radius)
|
||||||
|
&& Radius >= 0f
|
||||||
|
&& float.IsFinite(Height)
|
||||||
|
&& Height >= 0f
|
||||||
|
&& ShadowDisposition is RuntimeLocalPlayerShadowDisposition
|
||||||
|
.RegisteredAuthoredPayload
|
||||||
|
or RuntimeLocalPlayerShadowDisposition.ProvenShapeless;
|
||||||
|
}
|
||||||
|
|
||||||
internal readonly record struct RuntimeLocalPlayerPhysicsPublicationToken(
|
internal readonly record struct RuntimeLocalPlayerPhysicsPublicationToken(
|
||||||
RuntimeEntityKey Entity,
|
RuntimeEntityKey Entity,
|
||||||
RuntimeEntityPlacementToken Placement,
|
RuntimeEntityPlacementToken Placement,
|
||||||
|
|
@ -111,6 +132,12 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
|
||||||
{ get; init; }
|
{ get; init; }
|
||||||
internal required PlayerMovementController Controller { get; init; }
|
internal required PlayerMovementController Controller { get; init; }
|
||||||
internal required PhysicsBody Body { get; init; }
|
internal required PhysicsBody Body { get; init; }
|
||||||
|
internal required EntityPhysicsHost PhysicsHost { get; init; }
|
||||||
|
internal required MovementManager Movement { get; init; }
|
||||||
|
internal required MotionInterpreter Motion { get; init; }
|
||||||
|
internal required RuntimeLocalPlayerPhysicsActivationPreparation
|
||||||
|
ActivationPreparation { get; init; }
|
||||||
|
internal required Activation PreparedActivation { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class Activation
|
private sealed class Activation
|
||||||
|
|
@ -122,8 +149,15 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
|
||||||
{ get; init; }
|
{ get; init; }
|
||||||
internal required PlayerMovementController Controller { get; init; }
|
internal required PlayerMovementController Controller { get; init; }
|
||||||
internal required PhysicsBody Body { get; init; }
|
internal required PhysicsBody Body { get; init; }
|
||||||
|
internal required EntityPhysicsHost PhysicsHost { get; init; }
|
||||||
|
internal required MovementManager Movement { get; init; }
|
||||||
|
internal required MotionInterpreter Motion { get; init; }
|
||||||
|
internal required RuntimeLocalPlayerPhysicsActivationPreparation
|
||||||
|
ActivationPreparation { get; init; }
|
||||||
internal RuntimeLocalPlayerPhysicsActivationReceipt Receipt
|
internal RuntimeLocalPlayerPhysicsActivationReceipt Receipt
|
||||||
{ get; set; }
|
{ get; set; }
|
||||||
|
internal RuntimeDormantSetPositionCommitReceipt PendingFinalCommit
|
||||||
|
{ get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly RuntimeEntityDirectory _entities;
|
private readonly RuntimeEntityDirectory _entities;
|
||||||
|
|
@ -135,6 +169,7 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
|
||||||
private ulong _nextPublicationId;
|
private ulong _nextPublicationId;
|
||||||
private ulong _nextActivationId;
|
private ulong _nextActivationId;
|
||||||
private ulong _nextEvaluationId;
|
private ulong _nextEvaluationId;
|
||||||
|
private long _activationDispatchFailureCount;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
internal RuntimeLocalPlayerPhysicsPublicationState(
|
internal RuntimeLocalPlayerPhysicsPublicationState(
|
||||||
|
|
@ -154,14 +189,25 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
|
||||||
in RuntimeEntityPlacementToken placement,
|
in RuntimeEntityPlacementToken placement,
|
||||||
in RuntimeSetPositionCommand command,
|
in RuntimeSetPositionCommand command,
|
||||||
PlayerMovementConstructionOptions options,
|
PlayerMovementConstructionOptions options,
|
||||||
|
in RuntimeLocalPlayerPhysicsActivationPreparation activationPreparation,
|
||||||
out RuntimeLocalPlayerPhysicsPublicationToken token)
|
out RuntimeLocalPlayerPhysicsPublicationToken token)
|
||||||
{
|
{
|
||||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
ArgumentNullException.ThrowIfNull(record);
|
ArgumentNullException.ThrowIfNull(record);
|
||||||
token = default;
|
token = default;
|
||||||
if (!CanPrepare(record, placement, command))
|
if (!activationPreparation.IsValid
|
||||||
|
|| !CanPrepare(record, placement, command))
|
||||||
return RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority;
|
return RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority;
|
||||||
|
|
||||||
|
// Exhaustion is checked before allocating or replacing a private
|
||||||
|
// candidate. A checked overflow must not strand an unowned controller
|
||||||
|
// or discard an already-prepared exact receipt.
|
||||||
|
ulong publicationId = checked(_nextPublicationId + 1UL);
|
||||||
|
ulong activationId = checked(_nextActivationId + 1UL);
|
||||||
|
|
||||||
|
RuntimeLocalPlayerPhysicsActivationPreparation preparedActivation =
|
||||||
|
activationPreparation;
|
||||||
|
|
||||||
var controller = PlayerMovementController.CreatePublicationCandidate(
|
var controller = PlayerMovementController.CreatePublicationCandidate(
|
||||||
_physics.Engine,
|
_physics.Engine,
|
||||||
options);
|
options);
|
||||||
|
|
@ -177,6 +223,69 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
|
||||||
controller.SetBodyOrientation(command.Physics.Orientation);
|
controller.SetBodyOrientation(command.Physics.Orientation);
|
||||||
controller.ApplyPhysicsState(record.FinalPhysicsState);
|
controller.ApplyPhysicsState(record.FinalPhysicsState);
|
||||||
PhysicsBody body = controller.PhysicsBody;
|
PhysicsBody body = controller.PhysicsBody;
|
||||||
|
var physics = record.Snapshot.Physics;
|
||||||
|
body.Friction = NormalizeFriction(
|
||||||
|
physics?.Friction ?? record.Snapshot.Friction);
|
||||||
|
body.Elasticity = NormalizeElasticity(
|
||||||
|
physics?.Elasticity
|
||||||
|
?? record.Snapshot.Elasticity
|
||||||
|
?? body.Elasticity);
|
||||||
|
if (physics?.Velocity is { } initialVelocity)
|
||||||
|
body.set_velocity(initialVelocity);
|
||||||
|
if (physics?.AngularVelocity is { } initialOmega)
|
||||||
|
body.Omega = initialOmega;
|
||||||
|
MovementManager movement = controller.Movement;
|
||||||
|
MotionInterpreter motion = controller.Motion;
|
||||||
|
EntityPhysicsHost physicsHost = null!;
|
||||||
|
movement.MoveToFactory = () =>
|
||||||
|
{
|
||||||
|
var moveTo = new MoveToManager(
|
||||||
|
motion,
|
||||||
|
stopCompletely: () =>
|
||||||
|
_ = controller.StopCompletelyAtPhysicsObjectBoundary(),
|
||||||
|
getPosition: () => body.CellPosition,
|
||||||
|
getHeading: () => MoveToMath.GetHeading(body.Orientation),
|
||||||
|
setHeading: (heading, _) => body.Orientation =
|
||||||
|
MoveToMath.SetHeading(body.Orientation, heading),
|
||||||
|
getOwnRadius: () => preparedActivation.Radius,
|
||||||
|
getOwnHeight: () => preparedActivation.Height,
|
||||||
|
contact: () => body.InContact,
|
||||||
|
isInterpolating: static () => false,
|
||||||
|
getVelocity: () => body.Velocity,
|
||||||
|
getSelfId: () => record.ServerGuid,
|
||||||
|
setTarget: (context, target, radius, quantum) =>
|
||||||
|
physicsHost.SetTarget(context, target, radius, quantum),
|
||||||
|
clearTarget: () => physicsHost.ClearTarget(),
|
||||||
|
getTargetQuantum: () =>
|
||||||
|
physicsHost.TargetManager.GetTargetQuantum(),
|
||||||
|
setTargetQuantum: quantum =>
|
||||||
|
physicsHost.TargetManager.SetTargetQuantum(quantum),
|
||||||
|
curTime: () => controller.SimTimeSeconds);
|
||||||
|
moveTo.StickTo = (target, radius, height) =>
|
||||||
|
physicsHost.PositionManager.StickTo(target, radius, height);
|
||||||
|
moveTo.Unstick = physicsHost.PositionManager.UnStick;
|
||||||
|
return moveTo;
|
||||||
|
};
|
||||||
|
physicsHost = new EntityPhysicsHost(
|
||||||
|
record.ServerGuid,
|
||||||
|
getPosition: () => body.CellPosition,
|
||||||
|
getVelocity: () => body.Velocity,
|
||||||
|
getRadius: () => preparedActivation.Radius,
|
||||||
|
inContact: () => body.InContact,
|
||||||
|
minterpMaxSpeed: () => motion.GetAdjustedMaxSpeed(),
|
||||||
|
curTime: () => controller.SimTimeSeconds,
|
||||||
|
physicsTimerTime: () => controller.SimTimeSeconds,
|
||||||
|
getObjectA: id => _physics.TryGetPhysicsHost(id, out var host)
|
||||||
|
? host
|
||||||
|
: null,
|
||||||
|
handleUpdateTarget: movement.HandleUpdateTarget,
|
||||||
|
interruptCurrentMovement: () =>
|
||||||
|
movement.CancelMoveTo(WeenieError.ActionCancelled));
|
||||||
|
movement.MakeMoveToManager();
|
||||||
|
motion.UnstickFromObject = physicsHost.PositionManager.UnStick;
|
||||||
|
motion.InterruptCurrentMovement = () =>
|
||||||
|
movement.CancelMoveTo(WeenieError.ActionCancelled);
|
||||||
|
controller.PositionManager = physicsHost.PositionManager;
|
||||||
// This checkpoint publishes ownership only. The subsequent canonical
|
// This checkpoint publishes ownership only. The subsequent canonical
|
||||||
// SetPosition transaction is the sole authority which may enter the
|
// SetPosition transaction is the sole authority which may enter the
|
||||||
// body into world simulation and activate its ordinary workset.
|
// body into world simulation and activate its ordinary workset.
|
||||||
|
|
@ -194,16 +303,41 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
DiscardCurrent();
|
DiscardCurrent();
|
||||||
|
_nextPublicationId = publicationId;
|
||||||
|
_nextActivationId = activationId;
|
||||||
token = new RuntimeLocalPlayerPhysicsPublicationToken(
|
token = new RuntimeLocalPlayerPhysicsPublicationToken(
|
||||||
record.Key.Value,
|
record.Key.Value,
|
||||||
placement,
|
placement,
|
||||||
checked(++_nextPublicationId),
|
publicationId,
|
||||||
_identity.ServerGuid,
|
_identity.ServerGuid,
|
||||||
_identity.Revision,
|
_identity.Revision,
|
||||||
record.PhysicsOwnershipEpoch,
|
record.PhysicsOwnershipEpoch,
|
||||||
record.ObjectClockEpoch,
|
record.ObjectClockEpoch,
|
||||||
_movement.ControllerOwnershipEpoch,
|
_movement.ControllerOwnershipEpoch,
|
||||||
_entities.SessionLifetimeVersion);
|
_entities.SessionLifetimeVersion);
|
||||||
|
ulong expectedControllerEpoch = checked(
|
||||||
|
_movement.ControllerOwnershipEpoch + 1UL);
|
||||||
|
var activationEnvelope = new Activation
|
||||||
|
{
|
||||||
|
Token = new RuntimeLocalPlayerPhysicsActivationToken(
|
||||||
|
token.Entity,
|
||||||
|
token.Placement,
|
||||||
|
activationId,
|
||||||
|
token.LocalPlayerServerGuid,
|
||||||
|
token.LocalPlayerIdentityRevision,
|
||||||
|
checked(token.PhysicsOwnershipEpoch + 1UL),
|
||||||
|
token.ObjectClockEpoch,
|
||||||
|
expectedControllerEpoch,
|
||||||
|
token.SessionGenerationAuthority),
|
||||||
|
Record = record,
|
||||||
|
PlacementCommand = command,
|
||||||
|
Controller = controller,
|
||||||
|
Body = body,
|
||||||
|
PhysicsHost = physicsHost,
|
||||||
|
Movement = movement,
|
||||||
|
Motion = motion,
|
||||||
|
ActivationPreparation = preparedActivation,
|
||||||
|
};
|
||||||
_candidate = new Candidate
|
_candidate = new Candidate
|
||||||
{
|
{
|
||||||
Token = token,
|
Token = token,
|
||||||
|
|
@ -211,10 +345,27 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
|
||||||
PlacementCommand = command,
|
PlacementCommand = command,
|
||||||
Controller = controller,
|
Controller = controller,
|
||||||
Body = body,
|
Body = body,
|
||||||
|
PhysicsHost = physicsHost,
|
||||||
|
Movement = movement,
|
||||||
|
Motion = motion,
|
||||||
|
ActivationPreparation = preparedActivation,
|
||||||
|
PreparedActivation = activationEnvelope,
|
||||||
};
|
};
|
||||||
return RuntimeLocalPlayerPhysicsPublicationStatus.Prepared;
|
return RuntimeLocalPlayerPhysicsPublicationStatus.Prepared;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static float NormalizeFriction(float? value) =>
|
||||||
|
value is >= 0f and <= 1f && float.IsFinite(value.Value)
|
||||||
|
? value.Value
|
||||||
|
: PhysicsBody.DefaultFriction;
|
||||||
|
|
||||||
|
private static float NormalizeElasticity(float value)
|
||||||
|
{
|
||||||
|
if (float.IsNaN(value) || value <= 0f)
|
||||||
|
return 0f;
|
||||||
|
return MathF.Min(value, 0.1f);
|
||||||
|
}
|
||||||
|
|
||||||
internal RuntimeLocalPlayerPhysicsPublicationStatus Commit(
|
internal RuntimeLocalPlayerPhysicsPublicationStatus Commit(
|
||||||
in RuntimeLocalPlayerPhysicsPublicationToken token) =>
|
in RuntimeLocalPlayerPhysicsPublicationToken token) =>
|
||||||
Commit(token, out _);
|
Commit(token, out _);
|
||||||
|
|
@ -245,24 +396,8 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
|
||||||
candidate.Record.ObjectClock);
|
candidate.Record.ObjectClock);
|
||||||
candidate.Record.SetPhysicsBody(candidate.Body);
|
candidate.Record.SetPhysicsBody(candidate.Body);
|
||||||
_movement.CommitRuntimeOwnedController(candidate.Controller);
|
_movement.CommitRuntimeOwnedController(candidate.Controller);
|
||||||
activationToken = new RuntimeLocalPlayerPhysicsActivationToken(
|
activationToken = candidate.PreparedActivation.Token;
|
||||||
candidate.Token.Entity,
|
_activation = candidate.PreparedActivation;
|
||||||
candidate.Token.Placement,
|
|
||||||
checked(++_nextActivationId),
|
|
||||||
candidate.Token.LocalPlayerServerGuid,
|
|
||||||
candidate.Token.LocalPlayerIdentityRevision,
|
|
||||||
candidate.Record.PhysicsOwnershipEpoch,
|
|
||||||
candidate.Record.ObjectClockEpoch,
|
|
||||||
_movement.ControllerOwnershipEpoch,
|
|
||||||
_entities.SessionLifetimeVersion);
|
|
||||||
_activation = new Activation
|
|
||||||
{
|
|
||||||
Token = activationToken,
|
|
||||||
Record = candidate.Record,
|
|
||||||
PlacementCommand = candidate.PlacementCommand,
|
|
||||||
Controller = candidate.Controller,
|
|
||||||
Body = candidate.Body,
|
|
||||||
};
|
|
||||||
_candidate = null;
|
_candidate = null;
|
||||||
return RuntimeLocalPlayerPhysicsPublicationStatus.Committed;
|
return RuntimeLocalPlayerPhysicsPublicationStatus.Committed;
|
||||||
}
|
}
|
||||||
|
|
@ -291,6 +426,16 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
|
||||||
activation.PlacementCommand,
|
activation.PlacementCommand,
|
||||||
out RuntimeDormantSetPositionEvaluation placement))
|
out RuntimeDormantSetPositionEvaluation placement))
|
||||||
{
|
{
|
||||||
|
if (ReferenceEquals(_activation, activation)
|
||||||
|
&& IsActivationCurrent(activation)
|
||||||
|
&& _physics.SetPosition.IsDormantLocalActivationAwaitingCell(
|
||||||
|
activation.Record,
|
||||||
|
activation.Body,
|
||||||
|
token.Placement,
|
||||||
|
activation.PlacementCommand))
|
||||||
|
{
|
||||||
|
return RuntimeLocalPlayerPhysicsActivationStatus.DeferredCell;
|
||||||
|
}
|
||||||
if (ReferenceEquals(_activation, activation)
|
if (ReferenceEquals(_activation, activation)
|
||||||
&& !IsActivationCurrent(activation))
|
&& !IsActivationCurrent(activation))
|
||||||
{
|
{
|
||||||
|
|
@ -329,6 +474,304 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
|
||||||
receipt.Placement);
|
receipt.Placement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal RuntimeDormantSetPositionCommitStatus CommitActivation(
|
||||||
|
in RuntimeLocalPlayerPhysicsActivationReceipt receipt,
|
||||||
|
out RuntimePlacementProjectionToken projection)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
projection = default;
|
||||||
|
if (!receipt.IsValid
|
||||||
|
|| _activation is not { } activation
|
||||||
|
|| activation.Token != receipt.Token
|
||||||
|
|| activation.Receipt != receipt)
|
||||||
|
{
|
||||||
|
return RuntimeDormantSetPositionCommitStatus.RejectedAuthority;
|
||||||
|
}
|
||||||
|
if (activation.PendingFinalCommit.Status
|
||||||
|
is RuntimeDormantSetPositionCommitStatus
|
||||||
|
.AwaitingFinalShadowPreparation)
|
||||||
|
{
|
||||||
|
return FinalizeActivation(
|
||||||
|
activation,
|
||||||
|
activation.PendingFinalCommit,
|
||||||
|
out projection);
|
||||||
|
}
|
||||||
|
if (!IsActivationCurrent(activation)
|
||||||
|
|| !_physics.SetPosition.IsDormantLocalEvaluationCurrent(
|
||||||
|
activation.Record,
|
||||||
|
activation.Body,
|
||||||
|
receipt.Placement))
|
||||||
|
{
|
||||||
|
activation.Receipt = default;
|
||||||
|
return RuntimeDormantSetPositionCommitStatus.RejectedAuthority;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool provenShapeless = activation.ActivationPreparation
|
||||||
|
.ShadowDisposition
|
||||||
|
is RuntimeLocalPlayerShadowDisposition.ProvenShapeless;
|
||||||
|
if (!_physics.SetPosition.TryPrepareDormantLocalActivationCommit(
|
||||||
|
activation.Record,
|
||||||
|
activation.Body,
|
||||||
|
receipt.Placement,
|
||||||
|
provenShapeless,
|
||||||
|
out PreparedDormantSetPositionCommit? prepared)
|
||||||
|
|| prepared is null
|
||||||
|
|| !IsActivationCurrent(activation))
|
||||||
|
{
|
||||||
|
activation.Receipt = default;
|
||||||
|
return RuntimeDormantSetPositionCommitStatus.RejectedAuthority;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_physics.SetPosition.TryApplyDormantLocalActivationCommit(
|
||||||
|
activation.Record,
|
||||||
|
activation.Body,
|
||||||
|
activation.Controller,
|
||||||
|
activation.PhysicsHost,
|
||||||
|
prepared,
|
||||||
|
out RuntimeDormantSetPositionCommitReceipt committed))
|
||||||
|
{
|
||||||
|
return RuntimeDormantSetPositionCommitStatus.RejectedAuthority;
|
||||||
|
}
|
||||||
|
if (committed.Status is RuntimeDormantSetPositionCommitStatus.DeferredCell)
|
||||||
|
{
|
||||||
|
activation.Receipt = default;
|
||||||
|
_physics.SetPosition.DispatchDormantLocalActivationShadow(committed);
|
||||||
|
return committed.Status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Named retail SetPosition: contact prefix, ground edge, second
|
||||||
|
// acceleration/sliding, collision callbacks, physical response, then
|
||||||
|
// shadow/live publication.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
activation.Controller.BeginDormantSetPositionGroundPhase();
|
||||||
|
if (committed.HitGround)
|
||||||
|
activation.Movement.HitGround();
|
||||||
|
else if (committed.LeaveGround)
|
||||||
|
activation.Motion.LeaveGround();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_activationDispatchFailureCount++;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
activation.Controller.EndDormantSetPositionGroundPhase();
|
||||||
|
}
|
||||||
|
if (committed.Status is RuntimeDormantSetPositionCommitStatus
|
||||||
|
.AwaitingFinalShadowPreparation
|
||||||
|
&& (!IsActivationPrephaseEnvelopeCurrent(activation, committed)
|
||||||
|
|| !RefreshDormantVector(activation, committed)
|
||||||
|
|| !RefreshDormantState(activation)
|
||||||
|
|| !_physics.SetPosition.CommitDormantLocalActivationPostGround(
|
||||||
|
activation.Record,
|
||||||
|
activation.Body,
|
||||||
|
committed)))
|
||||||
|
{
|
||||||
|
AbortActivation(
|
||||||
|
activation, committed, collisionAlreadyDispatched: false);
|
||||||
|
return RuntimeDormantSetPositionCommitStatus.RejectedAuthority;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SetPositionCollisionBatchDispatchResult collisionDispatch =
|
||||||
|
_physics.SetPosition.DispatchDormantLocalActivationCollision(
|
||||||
|
committed);
|
||||||
|
if (collisionDispatch.Status
|
||||||
|
is not SetPositionCollisionBatchDispatchStatus.Completed)
|
||||||
|
{
|
||||||
|
AbortActivation(
|
||||||
|
activation, committed, collisionAlreadyDispatched: true);
|
||||||
|
return RuntimeDormantSetPositionCommitStatus.RejectedAuthority;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_activationDispatchFailureCount++;
|
||||||
|
AbortActivation(
|
||||||
|
activation, committed, collisionAlreadyDispatched: true);
|
||||||
|
return RuntimeDormantSetPositionCommitStatus.RejectedAuthority;
|
||||||
|
}
|
||||||
|
if (!IsActivationResponseEnvelopeCurrent(activation, committed))
|
||||||
|
{
|
||||||
|
AbortActivation(
|
||||||
|
activation, committed, collisionAlreadyDispatched: true);
|
||||||
|
return RuntimeDormantSetPositionCommitStatus.RejectedAuthority;
|
||||||
|
}
|
||||||
|
activation.Controller.RefreshDormantRuntimePhysicsState(
|
||||||
|
activation.Record.FinalPhysicsState,
|
||||||
|
recalculateAcceleration: false);
|
||||||
|
_ = RefreshDormantVector(activation, committed);
|
||||||
|
if (committed.Status is RuntimeDormantSetPositionCommitStatus
|
||||||
|
.AwaitingFinalShadowPreparation
|
||||||
|
&& !IsActivationPrephaseEnvelopeCurrent(activation, committed)
|
||||||
|
|| !_physics.SetPosition.CommitDormantLocalActivationPostCollision(
|
||||||
|
activation.Record,
|
||||||
|
activation.Body,
|
||||||
|
committed))
|
||||||
|
{
|
||||||
|
AbortActivation(
|
||||||
|
activation, committed, collisionAlreadyDispatched: true);
|
||||||
|
return RuntimeDormantSetPositionCommitStatus.RejectedAuthority;
|
||||||
|
}
|
||||||
|
if (committed.Status is RuntimeDormantSetPositionCommitStatus
|
||||||
|
.RejectedPlacement)
|
||||||
|
{
|
||||||
|
activation.Receipt = default;
|
||||||
|
activation.PendingFinalCommit = committed;
|
||||||
|
return committed.Status;
|
||||||
|
}
|
||||||
|
activation.PendingFinalCommit = committed;
|
||||||
|
return FinalizeActivation(activation, committed, out projection);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RuntimeDormantSetPositionCommitStatus FinalizeActivation(
|
||||||
|
Activation activation,
|
||||||
|
in RuntimeDormantSetPositionCommitReceipt prephase,
|
||||||
|
out RuntimePlacementProjectionToken projection)
|
||||||
|
{
|
||||||
|
projection = default;
|
||||||
|
if (!IsActivationPrephaseEnvelopeCurrent(activation, prephase))
|
||||||
|
{
|
||||||
|
AbortActivation(
|
||||||
|
activation, prephase, collisionAlreadyDispatched: true);
|
||||||
|
return RuntimeDormantSetPositionCommitStatus.RejectedAuthority;
|
||||||
|
}
|
||||||
|
activation.Controller.RefreshDormantRuntimePhysicsState(
|
||||||
|
activation.Record.FinalPhysicsState,
|
||||||
|
recalculateAcceleration: false);
|
||||||
|
bool provenShapeless = activation.ActivationPreparation
|
||||||
|
.ShadowDisposition is RuntimeLocalPlayerShadowDisposition.ProvenShapeless;
|
||||||
|
if (!_physics.SetPosition.TryPrepareDormantLocalActivationFinalCommit(
|
||||||
|
activation.Record,
|
||||||
|
activation.Body,
|
||||||
|
prephase,
|
||||||
|
provenShapeless,
|
||||||
|
out PreparedDormantActivationFinalCommit? prepared)
|
||||||
|
|| prepared is null)
|
||||||
|
{
|
||||||
|
if (IsActivationPrephaseEnvelopeCurrent(activation, prephase))
|
||||||
|
return prephase.Status;
|
||||||
|
AbortActivation(
|
||||||
|
activation, prephase, collisionAlreadyDispatched: true);
|
||||||
|
return RuntimeDormantSetPositionCommitStatus.RejectedAuthority;
|
||||||
|
}
|
||||||
|
if (!IsActivationPrephaseEnvelopeCurrent(activation, prephase))
|
||||||
|
{
|
||||||
|
AbortActivation(
|
||||||
|
activation, prephase, collisionAlreadyDispatched: true);
|
||||||
|
return RuntimeDormantSetPositionCommitStatus.RejectedAuthority;
|
||||||
|
}
|
||||||
|
if (!_physics.SetPosition.TryApplyDormantLocalActivationFinalCommit(
|
||||||
|
activation.Record,
|
||||||
|
activation.Body,
|
||||||
|
activation.Controller,
|
||||||
|
activation.PhysicsHost,
|
||||||
|
prephase,
|
||||||
|
prepared,
|
||||||
|
out RuntimeDormantSetPositionCommitReceipt committed))
|
||||||
|
{
|
||||||
|
return prephase.Status;
|
||||||
|
}
|
||||||
|
activation.Receipt = default;
|
||||||
|
activation.PendingFinalCommit = default;
|
||||||
|
_activation = null;
|
||||||
|
_physics.SetPosition.DispatchDormantLocalActivationShadow(committed);
|
||||||
|
if (!IsCommittedActivationSuffixCurrent(activation, committed))
|
||||||
|
return committed.Status;
|
||||||
|
_physics.SetPosition.DispatchDormantLocalActivationPlacement(committed);
|
||||||
|
projection = committed.Projection.Token;
|
||||||
|
return committed.Status;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsActivationPrephaseEnvelopeCurrent(
|
||||||
|
Activation activation,
|
||||||
|
in RuntimeDormantSetPositionCommitReceipt receipt) =>
|
||||||
|
IsActivationOwnershipEnvelopeCurrent(activation)
|
||||||
|
&& _physics.IsCollisionEvaluationFatalAuthorityCurrent(
|
||||||
|
receipt.CollisionAuthority)
|
||||||
|
&& _physics.SetPosition.IsDormantLocalActivationPrephaseCurrent(
|
||||||
|
activation.Record,
|
||||||
|
activation.Body,
|
||||||
|
receipt);
|
||||||
|
|
||||||
|
private bool IsActivationResponseEnvelopeCurrent(
|
||||||
|
Activation activation,
|
||||||
|
in RuntimeDormantSetPositionCommitReceipt receipt) =>
|
||||||
|
IsActivationOwnershipEnvelopeCurrent(activation)
|
||||||
|
&& _physics.IsCollisionEvaluationFatalAuthorityCurrent(
|
||||||
|
receipt.CollisionAuthority)
|
||||||
|
&& _physics.SetPosition.IsDormantLocalActivationResponseCurrent(
|
||||||
|
activation.Record,
|
||||||
|
activation.Body,
|
||||||
|
receipt);
|
||||||
|
|
||||||
|
private bool IsActivationOwnershipEnvelopeCurrent(
|
||||||
|
Activation activation) =>
|
||||||
|
activation.Controller.IsRuntimeOwnedDormant
|
||||||
|
&& !activation.Controller.IsDormantSetPositionGroundPhaseActive
|
||||||
|
&& activation.Controller.OwnsPhysicsBody(activation.Body)
|
||||||
|
&& _entities.SessionLifetimeVersion
|
||||||
|
== activation.Token.SessionGenerationAuthority
|
||||||
|
&& _entities.IsCurrent(activation.Record)
|
||||||
|
&& activation.Record.Key == activation.Token.Entity
|
||||||
|
&& !_identity.IsDisposed
|
||||||
|
&& _identity.ServerGuid == activation.Token.LocalPlayerServerGuid
|
||||||
|
&& _identity.ServerGuid == activation.Record.ServerGuid
|
||||||
|
&& _identity.Revision == activation.Token.LocalPlayerIdentityRevision
|
||||||
|
&& activation.Record.PhysicsOwnershipEpoch
|
||||||
|
== activation.Token.PhysicsOwnershipEpoch
|
||||||
|
&& activation.Record.ObjectClockEpoch
|
||||||
|
== activation.Token.ObjectClockEpoch
|
||||||
|
&& _movement.CanCommitRuntimeOwnedController(
|
||||||
|
activation.Token.ControllerOwnershipEpoch,
|
||||||
|
activation.Controller)
|
||||||
|
&& ReferenceEquals(activation.Record.PhysicsBody, activation.Body)
|
||||||
|
&& activation.Record.PhysicsHost is null
|
||||||
|
&& activation.Record.RemoteMotion is null
|
||||||
|
&& activation.Record.Projectile is null
|
||||||
|
&& !activation.Record.PhysicsBodyAcquisitionInProgress
|
||||||
|
&& !activation.Record.RemoteMotionBindingInProgress
|
||||||
|
&& !activation.Record.ProjectileBindingInProgress
|
||||||
|
&& !activation.Record.RequiresRemotePlacementRuntime
|
||||||
|
&& !activation.Record.DeleteAcceptedForTeardown;
|
||||||
|
|
||||||
|
private static bool RefreshDormantState(Activation activation)
|
||||||
|
{
|
||||||
|
activation.Controller.RefreshDormantRuntimePhysicsState(
|
||||||
|
activation.Record.FinalPhysicsState,
|
||||||
|
recalculateAcceleration: false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool RefreshDormantVector(
|
||||||
|
Activation activation,
|
||||||
|
in RuntimeDormantSetPositionCommitReceipt receipt)
|
||||||
|
{
|
||||||
|
if (activation.Record.VectorAuthorityVersion
|
||||||
|
== receipt.SourceVectorAuthorityVersion)
|
||||||
|
return true;
|
||||||
|
var physics = activation.Record.Snapshot.Physics;
|
||||||
|
activation.Controller.RefreshDormantRuntimeVector(
|
||||||
|
physics?.Velocity,
|
||||||
|
physics?.AngularVelocity);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AbortActivation(
|
||||||
|
Activation activation,
|
||||||
|
in RuntimeDormantSetPositionCommitReceipt receipt,
|
||||||
|
bool collisionAlreadyDispatched)
|
||||||
|
{
|
||||||
|
_physics.SetPosition.RetireDormantLocalActivation(
|
||||||
|
receipt,
|
||||||
|
collisionAlreadyDispatched);
|
||||||
|
activation.Receipt = default;
|
||||||
|
activation.PendingFinalCommit = default;
|
||||||
|
if (ReferenceEquals(_activation, activation))
|
||||||
|
DiscardActivation();
|
||||||
|
}
|
||||||
|
|
||||||
internal bool DiscardActivation(
|
internal bool DiscardActivation(
|
||||||
in RuntimeLocalPlayerPhysicsActivationToken token)
|
in RuntimeLocalPlayerPhysicsActivationToken token)
|
||||||
{
|
{
|
||||||
|
|
@ -389,6 +832,9 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
|
||||||
_activation is null ? 0 : 1,
|
_activation is null ? 0 : 1,
|
||||||
_nextPublicationId);
|
_nextPublicationId);
|
||||||
|
|
||||||
|
internal long ActivationDispatchFailureCount =>
|
||||||
|
_activationDispatchFailureCount;
|
||||||
|
|
||||||
internal void ResetSession()
|
internal void ResetSession()
|
||||||
{
|
{
|
||||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
|
@ -501,15 +947,67 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
|
||||||
&& !activation.Record.ProjectileBindingInProgress
|
&& !activation.Record.ProjectileBindingInProgress
|
||||||
&& !activation.Record.RequiresRemotePlacementRuntime
|
&& !activation.Record.RequiresRemotePlacementRuntime
|
||||||
&& !activation.Record.DeleteAcceptedForTeardown
|
&& !activation.Record.DeleteAcceptedForTeardown
|
||||||
&& _physics.SetPosition.IsExactPreparedPlacementCurrent(
|
&& _physics.SetPosition.IsDormantLocalActivationLeaseCurrent(
|
||||||
activation.Record,
|
activation.Record,
|
||||||
|
activation.Body,
|
||||||
activation.Token.Placement,
|
activation.Token.Placement,
|
||||||
activation.PlacementCommand);
|
activation.PlacementCommand);
|
||||||
|
|
||||||
|
private bool IsCommittedActivationSuffixCurrent(
|
||||||
|
Activation activation,
|
||||||
|
in RuntimeDormantSetPositionCommitReceipt receipt)
|
||||||
|
{
|
||||||
|
return activation.Token.ObjectClockEpoch != ulong.MaxValue
|
||||||
|
&& _entities.SessionLifetimeVersion
|
||||||
|
== activation.Token.SessionGenerationAuthority
|
||||||
|
&& _entities.IsCurrent(activation.Record)
|
||||||
|
&& activation.Record.Key == activation.Token.Entity
|
||||||
|
&& !_identity.IsDisposed
|
||||||
|
&& _identity.ServerGuid == activation.Token.LocalPlayerServerGuid
|
||||||
|
&& _identity.ServerGuid == activation.Record.ServerGuid
|
||||||
|
&& _identity.Revision
|
||||||
|
== activation.Token.LocalPlayerIdentityRevision
|
||||||
|
&& activation.Record.PhysicsOwnershipEpoch
|
||||||
|
== activation.Token.PhysicsOwnershipEpoch
|
||||||
|
&& activation.Record.ObjectClockEpoch
|
||||||
|
== activation.Token.ObjectClockEpoch + 1UL
|
||||||
|
&& _movement.ControllerOwnershipEpoch
|
||||||
|
== activation.Token.ControllerOwnershipEpoch
|
||||||
|
&& ReferenceEquals(_movement.Controller, activation.Controller)
|
||||||
|
&& activation.Controller.IsRuntimePublished
|
||||||
|
&& activation.Controller.OwnsPhysicsBody(activation.Body)
|
||||||
|
&& ReferenceEquals(
|
||||||
|
activation.Record.PhysicsBody,
|
||||||
|
activation.Body)
|
||||||
|
&& ReferenceEquals(
|
||||||
|
activation.Record.PhysicsHost,
|
||||||
|
activation.PhysicsHost)
|
||||||
|
&& activation.Record.RemoteMotion is null
|
||||||
|
&& activation.Record.Projectile is null
|
||||||
|
&& !activation.Record.DeleteAcceptedForTeardown
|
||||||
|
&& _physics.SetPosition.IsDormantLocalActivationCommitCurrent(
|
||||||
|
activation.Record,
|
||||||
|
activation.Body,
|
||||||
|
receipt);
|
||||||
|
}
|
||||||
|
|
||||||
private void DiscardActivation()
|
private void DiscardActivation()
|
||||||
{
|
{
|
||||||
Activation? activation = _activation;
|
Activation? activation = _activation;
|
||||||
_activation = null;
|
_activation = null;
|
||||||
|
if (activation is not null)
|
||||||
|
{
|
||||||
|
if (activation.PendingFinalCommit.Status
|
||||||
|
is not RuntimeDormantSetPositionCommitStatus.None)
|
||||||
|
{
|
||||||
|
_physics.SetPosition.RetireDormantLocalActivation(
|
||||||
|
activation.PendingFinalCommit,
|
||||||
|
collisionAlreadyDispatched: true);
|
||||||
|
}
|
||||||
|
_physics.SetPosition.RetireDormantLocalActivationToken(
|
||||||
|
activation.Record,
|
||||||
|
activation.Token.Placement);
|
||||||
|
}
|
||||||
if (activation is not null
|
if (activation is not null
|
||||||
&& _entities.IsCurrent(activation.Record)
|
&& _entities.IsCurrent(activation.Record)
|
||||||
&& ReferenceEquals(
|
&& ReferenceEquals(
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,17 @@ internal enum RuntimeCollisionReportKind
|
||||||
EnvironmentCollision,
|
EnvironmentCollision,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal enum SetPositionCollisionBatchDispatchStatus : byte
|
||||||
|
{
|
||||||
|
RejectedReceipt,
|
||||||
|
Displaced,
|
||||||
|
Completed,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal readonly record struct SetPositionCollisionBatchDispatchResult(
|
||||||
|
SetPositionCollisionBatchDispatchStatus Status,
|
||||||
|
bool Reported);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Immutable presentation-free projection of one retail weenie collision
|
/// Immutable presentation-free projection of one retail weenie collision
|
||||||
/// callback. Runtime commits the callback before an observer can re-enter.
|
/// callback. Runtime commits the callback before an observer can re-enter.
|
||||||
|
|
@ -38,6 +49,7 @@ internal readonly record struct RuntimeCollisionReportingOwnershipSnapshot(
|
||||||
int PendingReportCount,
|
int PendingReportCount,
|
||||||
int LeavingOwnerCount,
|
int LeavingOwnerCount,
|
||||||
int AdmissionBlockedOwnerCount,
|
int AdmissionBlockedOwnerCount,
|
||||||
|
int PendingSetPositionDispatchCount,
|
||||||
bool IsDispatching,
|
bool IsDispatching,
|
||||||
long DispatchFailureCount,
|
long DispatchFailureCount,
|
||||||
bool IsDisposed)
|
bool IsDisposed)
|
||||||
|
|
@ -51,6 +63,7 @@ internal readonly record struct RuntimeCollisionReportingOwnershipSnapshot(
|
||||||
&& PendingReportCount == 0
|
&& PendingReportCount == 0
|
||||||
&& LeavingOwnerCount == 0
|
&& LeavingOwnerCount == 0
|
||||||
&& AdmissionBlockedOwnerCount == 0
|
&& AdmissionBlockedOwnerCount == 0
|
||||||
|
&& PendingSetPositionDispatchCount == 0
|
||||||
&& !IsDispatching;
|
&& !IsDispatching;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,6 +91,10 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
|
||||||
private ulong _nextSequence;
|
private ulong _nextSequence;
|
||||||
private ulong _dispatchEpoch = 1UL;
|
private ulong _dispatchEpoch = 1UL;
|
||||||
private long _dispatchFailureCount;
|
private long _dispatchFailureCount;
|
||||||
|
private ulong _mutationRevision;
|
||||||
|
private ulong _nextPreparedBatchId;
|
||||||
|
private ulong _lastInstalledBatchId;
|
||||||
|
private readonly HashSet<ulong> _pendingSetPositionDispatches = [];
|
||||||
private bool _dispatching;
|
private bool _dispatching;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
|
|
@ -102,6 +119,7 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
|
||||||
_pendingReports.Count,
|
_pendingReports.Count,
|
||||||
_leaving.Count,
|
_leaving.Count,
|
||||||
_admissionBlocked.Count,
|
_admissionBlocked.Count,
|
||||||
|
_pendingSetPositionDispatches.Count,
|
||||||
_dispatching,
|
_dispatching,
|
||||||
_dispatchFailureCount,
|
_dispatchFailureCount,
|
||||||
_disposed);
|
_disposed);
|
||||||
|
|
@ -125,6 +143,574 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
|
||||||
return new Subscription(this, observer);
|
return new Subscription(this, observer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal enum StagedReportEligibility : byte
|
||||||
|
{
|
||||||
|
Environment,
|
||||||
|
Object,
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record FrozenCollisionSubject(
|
||||||
|
uint LocalEntityId,
|
||||||
|
bool IsStatic,
|
||||||
|
RuntimeEntityRecord? Record,
|
||||||
|
PhysicsBody? Body,
|
||||||
|
RuntimeEntityKey Key);
|
||||||
|
|
||||||
|
internal sealed record StagedReportAction(
|
||||||
|
StagedReportEligibility Eligibility,
|
||||||
|
RuntimeEntityRecord Recipient,
|
||||||
|
PhysicsBody RecipientBody,
|
||||||
|
RuntimeEntityKey RecipientKey,
|
||||||
|
RuntimeEntityRecord? Other,
|
||||||
|
PhysicsBody? OtherBody,
|
||||||
|
RuntimeEntityKey? OtherKey,
|
||||||
|
bool RecipientContact,
|
||||||
|
bool ExactDormantRecipient,
|
||||||
|
ulong RecipientPositionAuthorityVersion);
|
||||||
|
|
||||||
|
internal sealed class PreparedSetPositionCollisionBatch
|
||||||
|
{
|
||||||
|
internal required ulong BatchId { get; init; }
|
||||||
|
internal required ulong ExpectedMutationRevision { get; init; }
|
||||||
|
internal required ulong InstalledMutationRevision { get; init; }
|
||||||
|
internal required ulong SessionLifetimeVersion { get; init; }
|
||||||
|
internal required RuntimeEntityRecord Owner { get; init; }
|
||||||
|
internal required PhysicsBody OwnerBody { get; init; }
|
||||||
|
internal required RuntimeEntityKey OwnerKey { get; init; }
|
||||||
|
internal required ulong OwnerPositionAuthorityVersion { get; init; }
|
||||||
|
internal required OwnerState? OwnerState { get; init; }
|
||||||
|
internal required bool PreviousContact { get; init; }
|
||||||
|
internal required bool FinalCollidedWithEnvironment { get; init; }
|
||||||
|
internal required bool FinalGroundEdge { get; init; }
|
||||||
|
internal required double PhysicsTime { get; init; }
|
||||||
|
internal required StagedReportAction[] Actions { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal readonly record struct SetPositionCollisionBatchReceipt(
|
||||||
|
ulong BatchId,
|
||||||
|
RuntimeEntityRecord Owner,
|
||||||
|
PhysicsBody OwnerBody,
|
||||||
|
RuntimeEntityKey OwnerKey,
|
||||||
|
ulong OwnerPositionAuthorityVersion,
|
||||||
|
bool PreviousContact,
|
||||||
|
bool FinalCollidedWithEnvironment,
|
||||||
|
bool FinalGroundEdge,
|
||||||
|
double PhysicsTime,
|
||||||
|
StagedReportAction[] Actions)
|
||||||
|
{
|
||||||
|
internal bool IsValid => BatchId != 0UL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Freezes retail COLLISIONINFO subjects and prepares the owner-table,
|
||||||
|
/// environment-latch, and callback subjects without mutating Runtime.
|
||||||
|
/// Dynamic tracking and reverse-index writes occur during ordered dispatch
|
||||||
|
/// after each subject's live behavior flags are revalidated. Callback
|
||||||
|
/// eligibility is evaluated after the dormant
|
||||||
|
/// frame/contact/ground prephase and before physical response, shadow
|
||||||
|
/// publication, and enter-world activation, matching retail SetPosition.
|
||||||
|
/// </summary>
|
||||||
|
internal bool TryPrepareSetPositionBatch(
|
||||||
|
RuntimeEntityRecord owner,
|
||||||
|
PhysicsBody ownerBody,
|
||||||
|
double physicsTime,
|
||||||
|
bool previousContact,
|
||||||
|
bool previousOnWalkable,
|
||||||
|
bool finalOnWalkable,
|
||||||
|
bool collidedWithEnvironment,
|
||||||
|
ImmutableArray<uint> collidedObjectIds,
|
||||||
|
out PreparedSetPositionCollisionBatch? prepared)
|
||||||
|
{
|
||||||
|
EnsureNotDisposed();
|
||||||
|
ArgumentNullException.ThrowIfNull(owner);
|
||||||
|
ArgumentNullException.ThrowIfNull(ownerBody);
|
||||||
|
prepared = null;
|
||||||
|
RuntimeEntityKey ownerKey = owner.Key ?? default;
|
||||||
|
if (!double.IsFinite(physicsTime)
|
||||||
|
|| ownerKey == default
|
||||||
|
|| !IsKnownParticipant(owner, ownerBody, ownerKey)
|
||||||
|
|| _leaving.Contains(ownerKey)
|
||||||
|
|| _admissionBlocked.Contains(ownerKey))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong expectedMutation = _mutationRevision;
|
||||||
|
ulong installedMutation = checked(expectedMutation + 1UL);
|
||||||
|
ulong sessionLifetime = _entities.SessionLifetimeVersion;
|
||||||
|
if (collidedObjectIds.IsDefault)
|
||||||
|
collidedObjectIds = ImmutableArray<uint>.Empty;
|
||||||
|
var subjects = new List<FrozenCollisionSubject>(
|
||||||
|
collidedObjectIds.Length);
|
||||||
|
for (int index = 0; index < collidedObjectIds.Length; index++)
|
||||||
|
{
|
||||||
|
uint localId = collidedObjectIds[index];
|
||||||
|
if (localId == 0u || localId == ownerKey.LocalEntityId)
|
||||||
|
continue;
|
||||||
|
if (!_shadows.TryGetCollisionOwner(
|
||||||
|
localId,
|
||||||
|
out uint shadowState,
|
||||||
|
out bool isStatic))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isStatic)
|
||||||
|
{
|
||||||
|
subjects.Add(new FrozenCollisionSubject(
|
||||||
|
localId,
|
||||||
|
IsStatic: true,
|
||||||
|
Record: null,
|
||||||
|
Body: null,
|
||||||
|
Key: default));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!_entities.TryGetByLocalId(localId, out RuntimeEntityRecord target)
|
||||||
|
|| target.PhysicsBody is not { } targetBody
|
||||||
|
|| !_entities.IsCurrent(target)
|
||||||
|
|| target.Key is not { } targetKey)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
subjects.Add(new FrozenCollisionSubject(
|
||||||
|
localId,
|
||||||
|
IsStatic: false,
|
||||||
|
target,
|
||||||
|
targetBody,
|
||||||
|
targetKey));
|
||||||
|
_ = shadowState;
|
||||||
|
}
|
||||||
|
|
||||||
|
OwnerState staged = CloneOwnerState(TryGetOwner(ownerKey));
|
||||||
|
var actions = new List<StagedReportAction>(subjects.Count);
|
||||||
|
void StageEnvironment()
|
||||||
|
{
|
||||||
|
actions.Add(new StagedReportAction(
|
||||||
|
StagedReportEligibility.Environment,
|
||||||
|
owner,
|
||||||
|
ownerBody,
|
||||||
|
ownerKey,
|
||||||
|
Other: null,
|
||||||
|
OtherBody: null,
|
||||||
|
OtherKey: null,
|
||||||
|
RecipientContact: previousContact,
|
||||||
|
ExactDormantRecipient: !ownerBody.InWorld,
|
||||||
|
RecipientPositionAuthorityVersion:
|
||||||
|
owner.PositionAuthorityVersion));
|
||||||
|
}
|
||||||
|
for (int index = 0; index < subjects.Count; index++)
|
||||||
|
{
|
||||||
|
FrozenCollisionSubject subject = subjects[index];
|
||||||
|
if (subject.IsStatic)
|
||||||
|
{
|
||||||
|
StageEnvironment();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
RuntimeEntityRecord target = subject.Record!;
|
||||||
|
PhysicsBody targetBody = subject.Body!;
|
||||||
|
RuntimeEntityKey targetKey = subject.Key;
|
||||||
|
// Dynamic classification and behavior flags are evaluated after
|
||||||
|
// the ground edge. Retain the old record unchanged in the
|
||||||
|
// installed batch; the ordered tracking action below performs
|
||||||
|
// retail's timestamp/Ethereal clobber or Static conversion.
|
||||||
|
actions.Add(new StagedReportAction(
|
||||||
|
StagedReportEligibility.Object,
|
||||||
|
owner,
|
||||||
|
ownerBody,
|
||||||
|
ownerKey,
|
||||||
|
target,
|
||||||
|
targetBody,
|
||||||
|
targetKey,
|
||||||
|
RecipientContact: previousContact,
|
||||||
|
ExactDormantRecipient: !ownerBody.InWorld,
|
||||||
|
RecipientPositionAuthorityVersion:
|
||||||
|
owner.PositionAuthorityVersion));
|
||||||
|
}
|
||||||
|
|
||||||
|
_owners.EnsureCapacity(_owners.Count + 1);
|
||||||
|
_pendingSetPositionDispatches.EnsureCapacity(
|
||||||
|
_pendingSetPositionDispatches.Count + 1);
|
||||||
|
prepared = new PreparedSetPositionCollisionBatch
|
||||||
|
{
|
||||||
|
BatchId = checked(++_nextPreparedBatchId),
|
||||||
|
ExpectedMutationRevision = expectedMutation,
|
||||||
|
InstalledMutationRevision = installedMutation,
|
||||||
|
SessionLifetimeVersion = sessionLifetime,
|
||||||
|
Owner = owner,
|
||||||
|
OwnerBody = ownerBody,
|
||||||
|
OwnerKey = ownerKey,
|
||||||
|
OwnerPositionAuthorityVersion = owner.PositionAuthorityVersion,
|
||||||
|
OwnerState = staged.Records.Count == 0
|
||||||
|
&& !staged.CollidingWithEnvironment
|
||||||
|
? null
|
||||||
|
: staged,
|
||||||
|
PreviousContact = previousContact,
|
||||||
|
FinalCollidedWithEnvironment = collidedWithEnvironment,
|
||||||
|
FinalGroundEdge = !previousOnWalkable && finalOnWalkable,
|
||||||
|
PhysicsTime = physicsTime,
|
||||||
|
Actions = actions.ToArray(),
|
||||||
|
};
|
||||||
|
_ = previousContact;
|
||||||
|
return _mutationRevision == expectedMutation
|
||||||
|
&& _entities.SessionLifetimeVersion == sessionLifetime
|
||||||
|
&& IsKnownParticipant(owner, ownerBody, ownerKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool TryInstallSetPositionBatch(
|
||||||
|
PreparedSetPositionCollisionBatch prepared,
|
||||||
|
out SetPositionCollisionBatchReceipt receipt)
|
||||||
|
{
|
||||||
|
EnsureNotDisposed();
|
||||||
|
ArgumentNullException.ThrowIfNull(prepared);
|
||||||
|
receipt = default;
|
||||||
|
if (prepared.BatchId <= _lastInstalledBatchId
|
||||||
|
|| _mutationRevision != prepared.ExpectedMutationRevision
|
||||||
|
|| _entities.SessionLifetimeVersion
|
||||||
|
!= prepared.SessionLifetimeVersion
|
||||||
|
|| !IsKnownParticipant(
|
||||||
|
prepared.Owner,
|
||||||
|
prepared.OwnerBody,
|
||||||
|
prepared.OwnerKey))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!IsKnownParticipant(
|
||||||
|
prepared.Owner,
|
||||||
|
prepared.OwnerBody,
|
||||||
|
prepared.OwnerKey))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (prepared.OwnerState is null)
|
||||||
|
_owners.Remove(prepared.OwnerKey);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
prepared.OwnerState.SetPositionBatchId = prepared.BatchId;
|
||||||
|
_owners[prepared.OwnerKey] = prepared.OwnerState;
|
||||||
|
}
|
||||||
|
_mutationRevision = prepared.InstalledMutationRevision;
|
||||||
|
_lastInstalledBatchId = prepared.BatchId;
|
||||||
|
_pendingSetPositionDispatches.Add(prepared.BatchId);
|
||||||
|
receipt = new SetPositionCollisionBatchReceipt(
|
||||||
|
prepared.BatchId,
|
||||||
|
prepared.Owner,
|
||||||
|
prepared.OwnerBody,
|
||||||
|
prepared.OwnerKey,
|
||||||
|
prepared.OwnerPositionAuthorityVersion,
|
||||||
|
prepared.PreviousContact,
|
||||||
|
prepared.FinalCollidedWithEnvironment,
|
||||||
|
prepared.FinalGroundEdge,
|
||||||
|
prepared.PhysicsTime,
|
||||||
|
prepared.Actions);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool IsPreparedSetPositionBatchCurrent(
|
||||||
|
PreparedSetPositionCollisionBatch prepared)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(prepared);
|
||||||
|
return !_disposed
|
||||||
|
&& prepared.BatchId > _lastInstalledBatchId
|
||||||
|
&& _mutationRevision == prepared.ExpectedMutationRevision
|
||||||
|
&& _entities.SessionLifetimeVersion
|
||||||
|
== prepared.SessionLifetimeVersion
|
||||||
|
&& IsKnownParticipant(
|
||||||
|
prepared.Owner,
|
||||||
|
prepared.OwnerBody,
|
||||||
|
prepared.OwnerKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool DispatchSetPositionBatch(
|
||||||
|
in SetPositionCollisionBatchReceipt receipt) =>
|
||||||
|
DispatchSetPositionBatchResult(receipt).Reported;
|
||||||
|
|
||||||
|
internal SetPositionCollisionBatchDispatchResult
|
||||||
|
DispatchSetPositionBatchResult(
|
||||||
|
in SetPositionCollisionBatchReceipt receipt)
|
||||||
|
{
|
||||||
|
if (!receipt.IsValid
|
||||||
|
|| _disposed
|
||||||
|
|| receipt.BatchId > _lastInstalledBatchId
|
||||||
|
|| !_pendingSetPositionDispatches.Remove(receipt.BatchId))
|
||||||
|
{
|
||||||
|
return new(
|
||||||
|
SetPositionCollisionBatchDispatchStatus.RejectedReceipt,
|
||||||
|
Reported: false);
|
||||||
|
}
|
||||||
|
bool reported = false;
|
||||||
|
for (int index = 0; index < receipt.Actions.Length; index++)
|
||||||
|
{
|
||||||
|
if (receipt.Owner.PositionAuthorityVersion
|
||||||
|
!= receipt.OwnerPositionAuthorityVersion
|
||||||
|
|| _owners.TryGetValue(
|
||||||
|
receipt.OwnerKey, out OwnerState? currentOwner)
|
||||||
|
&& currentOwner.SetPositionBatchId != receipt.BatchId)
|
||||||
|
{
|
||||||
|
return new(
|
||||||
|
SetPositionCollisionBatchDispatchStatus.Displaced,
|
||||||
|
reported);
|
||||||
|
}
|
||||||
|
StagedReportAction action = receipt.Actions[index];
|
||||||
|
if (action.Eligibility is StagedReportEligibility.Environment)
|
||||||
|
{
|
||||||
|
reported |= DispatchEnvironmentAction(
|
||||||
|
action.Recipient,
|
||||||
|
action.RecipientBody,
|
||||||
|
action.RecipientKey,
|
||||||
|
action.RecipientContact,
|
||||||
|
receipt.BatchId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
reported |= DispatchTrackingAction(
|
||||||
|
action, receipt.PhysicsTime, receipt.BatchId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (receipt.Owner.PositionAuthorityVersion
|
||||||
|
!= receipt.OwnerPositionAuthorityVersion
|
||||||
|
|| _owners.TryGetValue(
|
||||||
|
receipt.OwnerKey, out OwnerState? suffixOwner)
|
||||||
|
&& suffixOwner.SetPositionBatchId != receipt.BatchId)
|
||||||
|
{
|
||||||
|
return new(
|
||||||
|
SetPositionCollisionBatchDispatchStatus.Displaced,
|
||||||
|
reported);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retail chooses the expired set only after every current collision
|
||||||
|
// has either refreshed its live Ethereal bit/timestamp or converted
|
||||||
|
// to environment. EndExpiredObjectCollisions predeletes the complete
|
||||||
|
// selected suffix before its first callback.
|
||||||
|
EndExpiredObjectCollisions(
|
||||||
|
receipt.Owner,
|
||||||
|
receipt.OwnerBody,
|
||||||
|
receipt.OwnerKey,
|
||||||
|
receipt.PhysicsTime,
|
||||||
|
force: false,
|
||||||
|
receipt.BatchId,
|
||||||
|
receipt.OwnerPositionAuthorityVersion);
|
||||||
|
if (!IsSetPositionBatchOwnerCurrent(receipt))
|
||||||
|
{
|
||||||
|
return new(
|
||||||
|
SetPositionCollisionBatchDispatchStatus.Displaced,
|
||||||
|
reported);
|
||||||
|
}
|
||||||
|
reported |= DispatchEnvironmentSuffix(receipt);
|
||||||
|
return new(
|
||||||
|
SetPositionCollisionBatchDispatchStatus.Completed,
|
||||||
|
reported);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool DiscardSetPositionBatch(
|
||||||
|
in SetPositionCollisionBatchReceipt receipt) =>
|
||||||
|
receipt.IsValid
|
||||||
|
&& _pendingSetPositionDispatches.Remove(receipt.BatchId);
|
||||||
|
|
||||||
|
internal void RetireSetPositionBatchOwner(
|
||||||
|
in SetPositionCollisionBatchReceipt receipt)
|
||||||
|
{
|
||||||
|
if (!receipt.IsValid || _disposed)
|
||||||
|
return;
|
||||||
|
_pendingSetPositionDispatches.Remove(receipt.BatchId);
|
||||||
|
if (!IsKnownParticipant(
|
||||||
|
receipt.Owner,
|
||||||
|
receipt.OwnerBody,
|
||||||
|
receipt.OwnerKey)
|
||||||
|
|| !_owners.TryGetValue(
|
||||||
|
receipt.OwnerKey, out OwnerState? owner)
|
||||||
|
|| owner.SetPositionBatchId != receipt.BatchId)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_mutationRevision = checked(_mutationRevision + 1UL);
|
||||||
|
if (!_admissionBlocked.Add(receipt.OwnerKey))
|
||||||
|
return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ForceEnd(receipt.Owner, receipt.OwnerKey);
|
||||||
|
if (_owners.TryGetValue(receipt.OwnerKey, out OwnerState? current)
|
||||||
|
&& ReferenceEquals(current, owner)
|
||||||
|
&& current.SetPositionBatchId == receipt.BatchId)
|
||||||
|
{
|
||||||
|
current.CollidingWithEnvironment = false;
|
||||||
|
_owners.Remove(receipt.OwnerKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_admissionBlocked.Remove(receipt.OwnerKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DispatchTrackingAction(
|
||||||
|
StagedReportAction action,
|
||||||
|
double physicsTime,
|
||||||
|
ulong batchId)
|
||||||
|
{
|
||||||
|
if (action.Other is null
|
||||||
|
|| action.OtherBody is null
|
||||||
|
|| action.OtherKey is not { } targetKey
|
||||||
|
|| !IsKnownParticipant(
|
||||||
|
action.Recipient,
|
||||||
|
action.RecipientBody,
|
||||||
|
action.RecipientKey)
|
||||||
|
|| !IsKnownParticipant(
|
||||||
|
action.Other,
|
||||||
|
action.OtherBody,
|
||||||
|
targetKey))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PhysicsStateFlags targetState = action.OtherBody.State;
|
||||||
|
if ((targetState & PhysicsStateFlags.Static) != 0)
|
||||||
|
{
|
||||||
|
// track_object_collision is skipped entirely on this path. Any
|
||||||
|
// older record therefore retains its old timestamp and remains
|
||||||
|
// eligible for the immediately-following expiry pass.
|
||||||
|
return DispatchEnvironmentAction(
|
||||||
|
action.Recipient,
|
||||||
|
action.RecipientBody,
|
||||||
|
action.RecipientKey,
|
||||||
|
action.RecipientContact,
|
||||||
|
batchId);
|
||||||
|
}
|
||||||
|
|
||||||
|
OwnerState state = GetOrCreateOwner(action.RecipientKey, batchId);
|
||||||
|
bool isNew = !state.Records.ContainsKey(targetKey);
|
||||||
|
state.Records[targetKey] = new CollisionRecord(
|
||||||
|
physicsTime,
|
||||||
|
(targetState & PhysicsStateFlags.Ethereal) != 0,
|
||||||
|
action.Other.ServerGuid);
|
||||||
|
if (!isNew)
|
||||||
|
{
|
||||||
|
_mutationRevision = checked(_mutationRevision + 1UL);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Order.Add(targetKey);
|
||||||
|
AddReverseOwner(targetKey, action.RecipientKey);
|
||||||
|
_mutationRevision = checked(_mutationRevision + 1UL);
|
||||||
|
return ReportObject(
|
||||||
|
action.Recipient,
|
||||||
|
action.RecipientBody,
|
||||||
|
action.RecipientKey,
|
||||||
|
action.Other,
|
||||||
|
action.OtherBody,
|
||||||
|
targetKey,
|
||||||
|
targetState,
|
||||||
|
action.RecipientContact,
|
||||||
|
action.ExactDormantRecipient,
|
||||||
|
action.RecipientPositionAuthorityVersion,
|
||||||
|
batchId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DispatchEnvironmentAction(
|
||||||
|
RuntimeEntityRecord owner,
|
||||||
|
PhysicsBody ownerBody,
|
||||||
|
RuntimeEntityKey ownerKey,
|
||||||
|
bool previousContact,
|
||||||
|
ulong batchId)
|
||||||
|
{
|
||||||
|
if (!IsKnownParticipant(owner, ownerBody, ownerKey))
|
||||||
|
return false;
|
||||||
|
OwnerState state = GetOrCreateOwner(ownerKey, batchId);
|
||||||
|
if (state.CollidingWithEnvironment)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
state.CollidingWithEnvironment = true;
|
||||||
|
_mutationRevision = checked(_mutationRevision + 1UL);
|
||||||
|
bool reported = (ownerBody.State
|
||||||
|
& PhysicsStateFlags.ReportCollisions) != 0;
|
||||||
|
if (reported)
|
||||||
|
{
|
||||||
|
Publish(new RuntimeCollisionReport(
|
||||||
|
NextSequence(),
|
||||||
|
RuntimeCollisionReportKind.EnvironmentCollision,
|
||||||
|
ownerKey,
|
||||||
|
owner.ServerGuid,
|
||||||
|
Other: null,
|
||||||
|
OtherServerGuid: null,
|
||||||
|
previousContact,
|
||||||
|
OtherWasInContact: false));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment collision tests Missile after the callback returns.
|
||||||
|
StopMissileForStagedOwner(
|
||||||
|
owner,
|
||||||
|
ownerBody,
|
||||||
|
ownerKey,
|
||||||
|
requireCurrentMissile: true);
|
||||||
|
return reported;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DispatchEnvironmentSuffix(
|
||||||
|
in SetPositionCollisionBatchReceipt receipt)
|
||||||
|
{
|
||||||
|
if (!IsSetPositionBatchOwnerCurrent(receipt)
|
||||||
|
|| !IsKnownParticipant(
|
||||||
|
receipt.Owner,
|
||||||
|
receipt.OwnerBody,
|
||||||
|
receipt.OwnerKey))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
OwnerState? state = TryGetOwner(receipt.OwnerKey);
|
||||||
|
if (state?.CollidingWithEnvironment == true)
|
||||||
|
{
|
||||||
|
if (state.CollidingWithEnvironment
|
||||||
|
!= receipt.FinalCollidedWithEnvironment)
|
||||||
|
{
|
||||||
|
state.CollidingWithEnvironment =
|
||||||
|
receipt.FinalCollidedWithEnvironment;
|
||||||
|
_mutationRevision = checked(_mutationRevision + 1UL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (receipt.FinalCollidedWithEnvironment
|
||||||
|
|| receipt.FinalGroundEdge)
|
||||||
|
{
|
||||||
|
bool reported = DispatchEnvironmentAction(
|
||||||
|
receipt.Owner,
|
||||||
|
receipt.OwnerBody,
|
||||||
|
receipt.OwnerKey,
|
||||||
|
receipt.PreviousContact,
|
||||||
|
receipt.BatchId);
|
||||||
|
TrimEmptyOwner(receipt.OwnerKey);
|
||||||
|
return reported;
|
||||||
|
}
|
||||||
|
TrimEmptyOwner(receipt.OwnerKey);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StopMissileForStagedOwner(
|
||||||
|
RuntimeEntityRecord owner,
|
||||||
|
PhysicsBody ownerBody,
|
||||||
|
RuntimeEntityKey ownerKey,
|
||||||
|
bool requireCurrentMissile)
|
||||||
|
{
|
||||||
|
if (!IsKnownParticipant(owner, ownerBody, ownerKey)
|
||||||
|
|| !_entities.StopMissileAfterCollision(
|
||||||
|
owner,
|
||||||
|
requireCurrentMissile))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_shadows.UpdatePhysicsState(
|
||||||
|
ownerKey.LocalEntityId,
|
||||||
|
(uint)owner.FinalPhysicsState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OwnerState CloneOwnerState(OwnerState? source)
|
||||||
|
{
|
||||||
|
var clone = new OwnerState();
|
||||||
|
if (source is null)
|
||||||
|
return clone;
|
||||||
|
foreach ((RuntimeEntityKey key, CollisionRecord record)
|
||||||
|
in source.Records)
|
||||||
|
clone.Records.Add(key, record);
|
||||||
|
clone.Order.AddRange(source.Order);
|
||||||
|
clone.CollidingWithEnvironment = source.CollidingWithEnvironment;
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ports the reporting/tracking portion of retail
|
/// Ports the reporting/tracking portion of retail
|
||||||
/// <c>CPhysicsObj::handle_all_collisions</c> (0x00514780). The return is
|
/// <c>CPhysicsObj::handle_all_collisions</c> (0x00514780). The return is
|
||||||
|
|
@ -148,6 +734,7 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
_mutationRevision = checked(_mutationRevision + 1UL);
|
||||||
|
|
||||||
bool reported = false;
|
bool reported = false;
|
||||||
if (collidedObjectIds.IsDefault)
|
if (collidedObjectIds.IsDefault)
|
||||||
|
|
@ -266,6 +853,7 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
|
||||||
ArgumentNullException.ThrowIfNull(record);
|
ArgumentNullException.ThrowIfNull(record);
|
||||||
if (record.Key is not { } key)
|
if (record.Key is not { } key)
|
||||||
return;
|
return;
|
||||||
|
_mutationRevision = checked(_mutationRevision + 1UL);
|
||||||
if (!_admissionBlocked.Add(key))
|
if (!_admissionBlocked.Add(key))
|
||||||
return;
|
return;
|
||||||
try
|
try
|
||||||
|
|
@ -288,6 +876,7 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
|
||||||
{
|
{
|
||||||
EnsureNotDisposed();
|
EnsureNotDisposed();
|
||||||
ArgumentNullException.ThrowIfNull(records);
|
ArgumentNullException.ThrowIfNull(records);
|
||||||
|
_mutationRevision = checked(_mutationRevision + 1UL);
|
||||||
var blocked = new List<(RuntimeEntityRecord Record, RuntimeEntityKey Key)>(
|
var blocked = new List<(RuntimeEntityRecord Record, RuntimeEntityKey Key)>(
|
||||||
records.Count);
|
records.Count);
|
||||||
for (int index = 0; index < records.Count; index++)
|
for (int index = 0; index < records.Count; index++)
|
||||||
|
|
@ -322,6 +911,7 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
|
||||||
ArgumentNullException.ThrowIfNull(record);
|
ArgumentNullException.ThrowIfNull(record);
|
||||||
if (record.Key is not { } key)
|
if (record.Key is not { } key)
|
||||||
return;
|
return;
|
||||||
|
_mutationRevision = checked(_mutationRevision + 1UL);
|
||||||
// A session-clear batch blocks every owner before publishing the
|
// A session-clear batch blocks every owner before publishing the
|
||||||
// first force-end callback. A callback may synchronously accept the
|
// first force-end callback. A callback may synchronously accept the
|
||||||
// deletion of a later, already-blocked owner. That owner must still
|
// deletion of a later, already-blocked owner. That owner must still
|
||||||
|
|
@ -342,11 +932,13 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
|
||||||
internal void ResetSession()
|
internal void ResetSession()
|
||||||
{
|
{
|
||||||
EnsureNotDisposed();
|
EnsureNotDisposed();
|
||||||
|
_mutationRevision = checked(_mutationRevision + 1UL);
|
||||||
_owners.Clear();
|
_owners.Clear();
|
||||||
_ownersByPeer.Clear();
|
_ownersByPeer.Clear();
|
||||||
_pendingReports.Clear();
|
_pendingReports.Clear();
|
||||||
_leaving.Clear();
|
_leaving.Clear();
|
||||||
_admissionBlocked.Clear();
|
_admissionBlocked.Clear();
|
||||||
|
_pendingSetPositionDispatches.Clear();
|
||||||
_dispatchEpoch = checked(_dispatchEpoch + 1UL);
|
_dispatchEpoch = checked(_dispatchEpoch + 1UL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -359,6 +951,7 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
|
||||||
_pendingReports.Clear();
|
_pendingReports.Clear();
|
||||||
_leaving.Clear();
|
_leaving.Clear();
|
||||||
_admissionBlocked.Clear();
|
_admissionBlocked.Clear();
|
||||||
|
_pendingSetPositionDispatches.Clear();
|
||||||
_observers = [];
|
_observers = [];
|
||||||
_dispatchEpoch = checked(_dispatchEpoch + 1UL);
|
_dispatchEpoch = checked(_dispatchEpoch + 1UL);
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
|
|
@ -372,7 +965,10 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
|
||||||
PhysicsBody targetBody,
|
PhysicsBody targetBody,
|
||||||
RuntimeEntityKey targetKey,
|
RuntimeEntityKey targetKey,
|
||||||
PhysicsStateFlags targetState,
|
PhysicsStateFlags targetState,
|
||||||
bool previousContact)
|
bool previousContact,
|
||||||
|
bool exactDormantOwner = false,
|
||||||
|
ulong expectedOwnerPositionAuthorityVersion = 0UL,
|
||||||
|
ulong setPositionBatchId = 0UL)
|
||||||
{
|
{
|
||||||
if ((targetState & PhysicsStateFlags.ReportAsEnvironment) != 0)
|
if ((targetState & PhysicsStateFlags.ReportAsEnvironment) != 0)
|
||||||
{
|
{
|
||||||
|
|
@ -380,7 +976,8 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
|
||||||
owner,
|
owner,
|
||||||
ownerBody,
|
ownerBody,
|
||||||
ownerKey,
|
ownerKey,
|
||||||
previousContact);
|
previousContact,
|
||||||
|
setPositionBatchId);
|
||||||
}
|
}
|
||||||
|
|
||||||
PhysicsStateFlags ownerState = ownerBody.State;
|
PhysicsStateFlags ownerState = ownerBody.State;
|
||||||
|
|
@ -416,7 +1013,13 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
|
||||||
// reentrant state update can therefore suppress this second report.
|
// reentrant state update can therefore suppress this second report.
|
||||||
bool targetReported = IsCurrentParticipant(target, targetBody, targetKey)
|
bool targetReported = IsCurrentParticipant(target, targetBody, targetKey)
|
||||||
&& (targetBody.State & PhysicsStateFlags.ReportCollisions) != 0
|
&& (targetBody.State & PhysicsStateFlags.ReportCollisions) != 0
|
||||||
&& IsCurrentParticipant(owner, ownerBody, ownerKey)
|
&& (IsCurrentParticipant(owner, ownerBody, ownerKey)
|
||||||
|
|| exactDormantOwner
|
||||||
|
&& IsExactDormantParticipant(
|
||||||
|
owner,
|
||||||
|
ownerBody,
|
||||||
|
ownerKey,
|
||||||
|
expectedOwnerPositionAuthorityVersion))
|
||||||
&& (ownerBody.State & PhysicsStateFlags.IgnoreCollisions) == 0;
|
&& (ownerBody.State & PhysicsStateFlags.IgnoreCollisions) == 0;
|
||||||
if (targetReported)
|
if (targetReported)
|
||||||
{
|
{
|
||||||
|
|
@ -433,13 +1036,31 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
|
||||||
return ownerReported || targetReported;
|
return ownerReported || targetReported;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsExactDormantParticipant(
|
||||||
|
RuntimeEntityRecord record,
|
||||||
|
PhysicsBody body,
|
||||||
|
RuntimeEntityKey key,
|
||||||
|
ulong expectedPositionAuthorityVersion) =>
|
||||||
|
!body.InWorld
|
||||||
|
&& (body.TransientState & TransientStateFlags.Active) == 0
|
||||||
|
&& (body.State & PhysicsStateFlags.Hidden) == 0
|
||||||
|
&& _entities.IsCurrent(record)
|
||||||
|
&& !_leaving.Contains(key)
|
||||||
|
&& !_admissionBlocked.Contains(key)
|
||||||
|
&& expectedPositionAuthorityVersion != 0UL
|
||||||
|
&& record.PositionAuthorityVersion
|
||||||
|
== expectedPositionAuthorityVersion
|
||||||
|
&& IsKnownParticipant(record, body, key);
|
||||||
|
|
||||||
private bool ReportEnvironment(
|
private bool ReportEnvironment(
|
||||||
RuntimeEntityRecord owner,
|
RuntimeEntityRecord owner,
|
||||||
PhysicsBody ownerBody,
|
PhysicsBody ownerBody,
|
||||||
RuntimeEntityKey ownerKey,
|
RuntimeEntityKey ownerKey,
|
||||||
bool previousContact)
|
bool previousContact,
|
||||||
|
ulong setPositionBatchId = 0UL)
|
||||||
{
|
{
|
||||||
OwnerState state = GetOrCreateOwner(ownerKey);
|
OwnerState state = GetOrCreateOwner(
|
||||||
|
ownerKey, setPositionBatchId);
|
||||||
if (state.CollidingWithEnvironment)
|
if (state.CollidingWithEnvironment)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
|
@ -467,7 +1088,9 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
|
||||||
PhysicsBody? ownerBody,
|
PhysicsBody? ownerBody,
|
||||||
RuntimeEntityKey ownerKey,
|
RuntimeEntityKey ownerKey,
|
||||||
double physicsTime,
|
double physicsTime,
|
||||||
bool force)
|
bool force,
|
||||||
|
ulong setPositionBatchId = 0UL,
|
||||||
|
ulong expectedPositionAuthorityVersion = 0UL)
|
||||||
{
|
{
|
||||||
if (!_owners.TryGetValue(ownerKey, out OwnerState? state)
|
if (!_owners.TryGetValue(ownerKey, out OwnerState? state)
|
||||||
|| state.Records.Count == 0)
|
|| state.Records.Count == 0)
|
||||||
|
|
@ -524,7 +1147,15 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
|
||||||
|| reportEpoch != _dispatchEpoch
|
|| reportEpoch != _dispatchEpoch
|
||||||
|| _entities.SessionLifetimeVersion != sourceSessionVersion
|
|| _entities.SessionLifetimeVersion != sourceSessionVersion
|
||||||
|| _entities.CurrentLifetimeMutation(owner.ServerGuid)
|
|| _entities.CurrentLifetimeMutation(owner.ServerGuid)
|
||||||
!= sourceLifetimeMutation)
|
!= sourceLifetimeMutation
|
||||||
|
|| setPositionBatchId != 0UL
|
||||||
|
&& (owner.PositionAuthorityVersion
|
||||||
|
!= expectedPositionAuthorityVersion
|
||||||
|
|| !_owners.TryGetValue(
|
||||||
|
ownerKey, out OwnerState? currentOwner)
|
||||||
|
|| !ReferenceEquals(currentOwner, state)
|
||||||
|
|| currentOwner.SetPositionBatchId
|
||||||
|
!= setPositionBatchId))
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -563,6 +1194,13 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
|
||||||
TrimEmptyOwner(ownerKey);
|
TrimEmptyOwner(ownerKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsSetPositionBatchOwnerCurrent(
|
||||||
|
in SetPositionCollisionBatchReceipt receipt) =>
|
||||||
|
receipt.Owner.PositionAuthorityVersion
|
||||||
|
== receipt.OwnerPositionAuthorityVersion
|
||||||
|
&& (!_owners.TryGetValue(receipt.OwnerKey, out OwnerState? owner)
|
||||||
|
|| owner.SetPositionBatchId == receipt.BatchId);
|
||||||
|
|
||||||
private void PublishResolvedObjectEnd(
|
private void PublishResolvedObjectEnd(
|
||||||
RuntimeEntityRecord owner,
|
RuntimeEntityRecord owner,
|
||||||
PhysicsBody? ownerBody,
|
PhysicsBody? ownerBody,
|
||||||
|
|
@ -654,13 +1292,16 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
|
||||||
(uint)owner.FinalPhysicsState);
|
(uint)owner.FinalPhysicsState);
|
||||||
}
|
}
|
||||||
|
|
||||||
private OwnerState GetOrCreateOwner(RuntimeEntityKey key)
|
private OwnerState GetOrCreateOwner(
|
||||||
|
RuntimeEntityKey key,
|
||||||
|
ulong setPositionBatchId = 0UL)
|
||||||
{
|
{
|
||||||
if (!_owners.TryGetValue(key, out OwnerState? owner))
|
if (!_owners.TryGetValue(key, out OwnerState? owner))
|
||||||
{
|
{
|
||||||
owner = new OwnerState();
|
owner = new OwnerState();
|
||||||
_owners.Add(key, owner);
|
_owners.Add(key, owner);
|
||||||
}
|
}
|
||||||
|
owner.SetPositionBatchId = setPositionBatchId;
|
||||||
return owner;
|
return owner;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -874,15 +1515,16 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
|
||||||
private void EnsureNotDisposed() =>
|
private void EnsureNotDisposed() =>
|
||||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
|
||||||
private sealed class OwnerState
|
internal sealed class OwnerState
|
||||||
{
|
{
|
||||||
internal Dictionary<RuntimeEntityKey, CollisionRecord> Records { get; }
|
internal Dictionary<RuntimeEntityKey, CollisionRecord> Records { get; }
|
||||||
= new();
|
= new();
|
||||||
internal List<RuntimeEntityKey> Order { get; } = [];
|
internal List<RuntimeEntityKey> Order { get; } = [];
|
||||||
internal bool CollidingWithEnvironment { get; set; }
|
internal bool CollidingWithEnvironment { get; set; }
|
||||||
|
internal ulong SetPositionBatchId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly record struct CollisionRecord(
|
internal readonly record struct CollisionRecord(
|
||||||
double TouchedTime,
|
double TouchedTime,
|
||||||
bool Ethereal,
|
bool Ethereal,
|
||||||
uint ServerGuid);
|
uint ServerGuid);
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ public readonly record struct RuntimePhysicsOwnershipSnapshot(
|
||||||
int PendingCollisionReportCount,
|
int PendingCollisionReportCount,
|
||||||
int LeavingCollisionReportOwnerCount,
|
int LeavingCollisionReportOwnerCount,
|
||||||
int CollisionReportAdmissionBlockedOwnerCount,
|
int CollisionReportAdmissionBlockedOwnerCount,
|
||||||
|
int PendingCollisionSetPositionDispatchCount,
|
||||||
|
int PendingShadowSetPositionDispatchCount,
|
||||||
bool IsCollisionReportDispatching,
|
bool IsCollisionReportDispatching,
|
||||||
int CollisionAdmissionCount,
|
int CollisionAdmissionCount,
|
||||||
int CollisionGenerationCount,
|
int CollisionGenerationCount,
|
||||||
|
|
@ -66,6 +68,8 @@ public readonly record struct RuntimePhysicsOwnershipSnapshot(
|
||||||
&& PendingCollisionReportCount == 0
|
&& PendingCollisionReportCount == 0
|
||||||
&& LeavingCollisionReportOwnerCount == 0
|
&& LeavingCollisionReportOwnerCount == 0
|
||||||
&& CollisionReportAdmissionBlockedOwnerCount == 0
|
&& CollisionReportAdmissionBlockedOwnerCount == 0
|
||||||
|
&& PendingCollisionSetPositionDispatchCount == 0
|
||||||
|
&& PendingShadowSetPositionDispatchCount == 0
|
||||||
&& !IsCollisionReportDispatching
|
&& !IsCollisionReportDispatching
|
||||||
&& CollisionAdmissionCount == 0
|
&& CollisionAdmissionCount == 0
|
||||||
&& CollisionGenerationCount == 0
|
&& CollisionGenerationCount == 0
|
||||||
|
|
@ -1162,6 +1166,8 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
collisionReports.PendingReportCount,
|
collisionReports.PendingReportCount,
|
||||||
collisionReports.LeavingOwnerCount,
|
collisionReports.LeavingOwnerCount,
|
||||||
collisionReports.AdmissionBlockedOwnerCount,
|
collisionReports.AdmissionBlockedOwnerCount,
|
||||||
|
collisionReports.PendingSetPositionDispatchCount,
|
||||||
|
Engine.ShadowObjects.PendingSetPositionDispatchCount,
|
||||||
collisionReports.IsDispatching,
|
collisionReports.IsDispatching,
|
||||||
_collisionAdmissions.Count,
|
_collisionAdmissions.Count,
|
||||||
_collisionGenerations.Count,
|
_collisionGenerations.Count,
|
||||||
|
|
@ -2529,6 +2535,18 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
TrimCollisionOwnerJournal();
|
TrimCollisionOwnerJournal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal bool TryPrepareSpatialRootAdmission(RuntimeEntityRecord record)
|
||||||
|
{
|
||||||
|
EnsureNotDisposed();
|
||||||
|
ArgumentNullException.ThrowIfNull(record);
|
||||||
|
if (record.Key is null || !Entities.IsCurrent(record))
|
||||||
|
return false;
|
||||||
|
_spatialRoots.EnsureCapacity(_spatialRoots.Count + 1);
|
||||||
|
_spatialRemotes.EnsureCapacity(_spatialRemotes.Count + 1);
|
||||||
|
_spatialProjectiles.EnsureCapacity(_spatialProjectiles.Count + 1);
|
||||||
|
return Entities.IsCurrent(record);
|
||||||
|
}
|
||||||
|
|
||||||
internal ulong ExpectedCollisionGeneration(uint exactCellId)
|
internal ulong ExpectedCollisionGeneration(uint exactCellId)
|
||||||
{
|
{
|
||||||
uint landblockId = CanonicalLandblock(exactCellId);
|
uint landblockId = CanonicalLandblock(exactCellId);
|
||||||
|
|
@ -2671,6 +2689,32 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal bool IsCollisionEvaluationFatalAuthorityCurrent(
|
||||||
|
in RuntimeCollisionEvaluationAuthority authority)
|
||||||
|
{
|
||||||
|
if (!authority.IsValid
|
||||||
|
|| _collisionWorldAuthority != authority.CollisionWorldAuthority
|
||||||
|
|| !ReferenceEquals(ObjectTable, authority.ObjectTable)
|
||||||
|
|| ObjectTableBindingAuthority
|
||||||
|
!= authority.ObjectTableBindingAuthority
|
||||||
|
|| ObjectTableAuthority != authority.ObjectTableAuthority)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
foreach (RuntimeCollisionGenerationAuthority generation
|
||||||
|
in authority.Generations)
|
||||||
|
{
|
||||||
|
if (generation.LandblockId == 0u
|
||||||
|
|| _collisionAdmissions.ContainsKey(generation.LandblockId)
|
||||||
|
|| CollisionGenerationAuthority(generation.LandblockId)
|
||||||
|
!= generation.Generation)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
internal bool HandleSetPositionCollisions(
|
internal bool HandleSetPositionCollisions(
|
||||||
RuntimeEntityRecord record,
|
RuntimeEntityRecord record,
|
||||||
ulong positionAuthorityVersion,
|
ulong positionAuthorityVersion,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using AcDream.Core.Items;
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Runtime.Entities;
|
using AcDream.Runtime.Entities;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
namespace AcDream.Runtime.Physics;
|
namespace AcDream.Runtime.Physics;
|
||||||
|
|
||||||
|
|
@ -29,6 +30,7 @@ internal enum RuntimeEntityPlacementStage
|
||||||
AwaitingPreparation,
|
AwaitingPreparation,
|
||||||
AwaitingWithdrawalAcknowledgement,
|
AwaitingWithdrawalAcknowledgement,
|
||||||
AwaitingCell,
|
AwaitingCell,
|
||||||
|
AwaitingFinalShadowPreparation,
|
||||||
AwaitingCommitAcknowledgement,
|
AwaitingCommitAcknowledgement,
|
||||||
CancelledAwaitingAcknowledgement,
|
CancelledAwaitingAcknowledgement,
|
||||||
}
|
}
|
||||||
|
|
@ -109,6 +111,65 @@ internal readonly record struct RuntimeDormantSetPositionEvaluation(
|
||||||
&& CollisionAuthority.IsValid;
|
&& CollisionAuthority.IsValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal enum RuntimeDormantSetPositionCommitStatus : byte
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
AwaitingFinalShadowPreparation,
|
||||||
|
Committed,
|
||||||
|
DeferredCell,
|
||||||
|
RejectedPlacement,
|
||||||
|
RejectedAuthority,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class PreparedDormantSetPositionCommit
|
||||||
|
{
|
||||||
|
internal required RuntimeDormantSetPositionEvaluation Evaluation
|
||||||
|
{ get; init; }
|
||||||
|
internal required RuntimeEntityKey Entity { get; init; }
|
||||||
|
internal required ulong OperationId { get; init; }
|
||||||
|
internal required ulong ExpectedProjectionSequence { get; init; }
|
||||||
|
internal required ShadowObjectRegistry.PreparedSetPositionShadowCommit?
|
||||||
|
Shadow { get; init; }
|
||||||
|
internal required RuntimeCollisionReportingState
|
||||||
|
.PreparedSetPositionCollisionBatch? Collision { get; init; }
|
||||||
|
internal required RuntimePlacementProjectionSnapshot Projection
|
||||||
|
{ get; init; }
|
||||||
|
internal required SortedDictionary<ulong, RuntimePlacementProjectionSnapshot>?
|
||||||
|
PendingProjection { get; init; }
|
||||||
|
internal required ulong DeferredCollisionGeneration { get; init; }
|
||||||
|
internal required List<RuntimeEntityKey>? DeferredBucket { get; init; }
|
||||||
|
internal required bool DeferredBucketIsNew { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class PreparedDormantActivationFinalCommit
|
||||||
|
{
|
||||||
|
internal required RuntimeEntityKey Entity { get; init; }
|
||||||
|
internal required ulong OperationId { get; init; }
|
||||||
|
internal required ulong ExpectedProjectionSequence { get; init; }
|
||||||
|
internal required ShadowObjectRegistry.PreparedSetPositionShadowCommit
|
||||||
|
Shadow { get; init; }
|
||||||
|
internal required RuntimePlacementProjectionSnapshot Projection
|
||||||
|
{ get; init; }
|
||||||
|
internal required SortedDictionary<ulong, RuntimePlacementProjectionSnapshot>
|
||||||
|
PendingProjection { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal readonly record struct RuntimeDormantSetPositionCommitReceipt(
|
||||||
|
RuntimeDormantSetPositionCommitStatus Status,
|
||||||
|
RuntimeEntityKey Entity,
|
||||||
|
ulong OperationId,
|
||||||
|
RuntimePlacementProjectionSnapshot Projection,
|
||||||
|
RuntimeCollisionReportingState.SetPositionCollisionBatchReceipt Collision,
|
||||||
|
ShadowObjectRegistry.SetPositionShadowCommitReceipt Shadow,
|
||||||
|
RuntimeCollisionEvaluationAuthority CollisionAuthority,
|
||||||
|
ulong SourceVectorAuthorityVersion,
|
||||||
|
bool HitGround,
|
||||||
|
bool LeaveGround)
|
||||||
|
{
|
||||||
|
internal bool IsCommitted => Status
|
||||||
|
is RuntimeDormantSetPositionCommitStatus.Committed;
|
||||||
|
}
|
||||||
|
|
||||||
public readonly record struct RuntimePlacementProjectionToken(
|
public readonly record struct RuntimePlacementProjectionToken(
|
||||||
ulong Sequence,
|
ulong Sequence,
|
||||||
ulong Revision,
|
ulong Revision,
|
||||||
|
|
@ -258,6 +319,7 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
internal List<RuntimeEntityKey>? LostFamilyKeys { get; set; }
|
internal List<RuntimeEntityKey>? LostFamilyKeys { get; set; }
|
||||||
internal bool InheritedLostDeadline { get; set; }
|
internal bool InheritedLostDeadline { get; set; }
|
||||||
internal bool EnteringWorldFromCelllessResidence { get; set; }
|
internal bool EnteringWorldFromCelllessResidence { get; set; }
|
||||||
|
internal bool DormantLocalActivation { get; set; }
|
||||||
internal RuntimeSetPositionCommand? PreparedCommandAwaitingWithdrawalAck
|
internal RuntimeSetPositionCommand? PreparedCommandAwaitingWithdrawalAck
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
|
|
@ -289,7 +351,7 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
private readonly Dictionary<RuntimeEntityKey, Operation> _operations = [];
|
private readonly Dictionary<RuntimeEntityKey, Operation> _operations = [];
|
||||||
private readonly Dictionary<CellGenerationKey, List<RuntimeEntityKey>>
|
private readonly Dictionary<CellGenerationKey, List<RuntimeEntityKey>>
|
||||||
_deferredByCellGeneration = [];
|
_deferredByCellGeneration = [];
|
||||||
private readonly SortedDictionary<ulong, RuntimePlacementProjectionSnapshot>
|
private SortedDictionary<ulong, RuntimePlacementProjectionSnapshot>
|
||||||
_pendingProjection = [];
|
_pendingProjection = [];
|
||||||
private readonly List<CellGenerationKey> _deferredBucketOrder = [];
|
private readonly List<CellGenerationKey> _deferredBucketOrder = [];
|
||||||
private readonly Dictionary<uint, List<RuntimeEntityKey>>
|
private readonly Dictionary<uint, List<RuntimeEntityKey>>
|
||||||
|
|
@ -558,8 +620,12 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
if (!token.IsValid
|
if (!token.IsValid
|
||||||
|| !_operations.TryGetValue(token.Entity, out Operation? operation)
|
|| !_operations.TryGetValue(token.Entity, out Operation? operation)
|
||||||
|| operation.Token != token
|
|| operation.Token != token
|
||||||
|| operation.Stage
|
|| operation.Stage is not (
|
||||||
is not RuntimeEntityPlacementStage.AwaitingPreparation
|
RuntimeEntityPlacementStage.AwaitingPreparation
|
||||||
|
or RuntimeEntityPlacementStage.AwaitingCell)
|
||||||
|
|| operation.Stage is RuntimeEntityPlacementStage.AwaitingCell
|
||||||
|
&& (!operation.DormantLocalActivation
|
||||||
|
|| !operation.WakeableLostCell)
|
||||||
|| !IsCurrent(operation)
|
|| !IsCurrent(operation)
|
||||||
|| !_moverPreparationAuthorities.TryGetValue(
|
|| !_moverPreparationAuthorities.TryGetValue(
|
||||||
token.Entity,
|
token.Entity,
|
||||||
|
|
@ -641,6 +707,20 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
ArgumentNullException.ThrowIfNull(record);
|
ArgumentNullException.ThrowIfNull(record);
|
||||||
ArgumentNullException.ThrowIfNull(body);
|
ArgumentNullException.ThrowIfNull(body);
|
||||||
evaluation = default;
|
evaluation = default;
|
||||||
|
if (!IsExactDormantLocalActivationCurrent(
|
||||||
|
record,
|
||||||
|
body,
|
||||||
|
token,
|
||||||
|
command,
|
||||||
|
out _)
|
||||||
|
&& !TryRearmDeferredDormantLocalActivation(
|
||||||
|
record,
|
||||||
|
body,
|
||||||
|
token,
|
||||||
|
command))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!IsExactDormantLocalActivationCurrent(
|
if (!IsExactDormantLocalActivationCurrent(
|
||||||
record,
|
record,
|
||||||
body,
|
body,
|
||||||
|
|
@ -716,6 +796,779 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
&& _physics.IsCollisionEvaluationAuthorityCurrent(
|
&& _physics.IsCollisionEvaluationAuthorityCurrent(
|
||||||
evaluation.CollisionAuthority);
|
evaluation.CollisionAuthority);
|
||||||
|
|
||||||
|
internal bool IsDormantLocalActivationLeaseCurrent(
|
||||||
|
RuntimeEntityRecord record,
|
||||||
|
PhysicsBody body,
|
||||||
|
in RuntimeEntityPlacementToken token,
|
||||||
|
in RuntimeSetPositionCommand command) =>
|
||||||
|
IsExactDormantLocalActivationCurrent(
|
||||||
|
record,
|
||||||
|
body,
|
||||||
|
token,
|
||||||
|
command,
|
||||||
|
out _,
|
||||||
|
allowDeferredLease: true);
|
||||||
|
|
||||||
|
internal bool IsDormantLocalActivationAwaitingCell(
|
||||||
|
RuntimeEntityRecord record,
|
||||||
|
PhysicsBody body,
|
||||||
|
in RuntimeEntityPlacementToken token,
|
||||||
|
in RuntimeSetPositionCommand command)
|
||||||
|
{
|
||||||
|
return IsExactDormantLocalActivationCurrent(
|
||||||
|
record,
|
||||||
|
body,
|
||||||
|
token,
|
||||||
|
command,
|
||||||
|
out Operation? operation,
|
||||||
|
allowDeferredLease: true)
|
||||||
|
&& operation is not null
|
||||||
|
&& operation.Stage is RuntimeEntityPlacementStage.AwaitingCell
|
||||||
|
&& operation.DormantLocalActivation
|
||||||
|
&& operation.WakeableLostCell;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryRearmDeferredDormantLocalActivation(
|
||||||
|
RuntimeEntityRecord record,
|
||||||
|
PhysicsBody body,
|
||||||
|
in RuntimeEntityPlacementToken token,
|
||||||
|
in RuntimeSetPositionCommand command)
|
||||||
|
{
|
||||||
|
if (!IsExactDormantLocalActivationCurrent(
|
||||||
|
record,
|
||||||
|
body,
|
||||||
|
token,
|
||||||
|
command,
|
||||||
|
out Operation? operation,
|
||||||
|
allowDeferredLease: true)
|
||||||
|
|| operation is null
|
||||||
|
|| operation.Stage is not RuntimeEntityPlacementStage.AwaitingCell
|
||||||
|
|| !operation.DormantLocalActivation
|
||||||
|
|| !operation.WakeableLostCell
|
||||||
|
|| !operation.CollisionGenerationReady
|
||||||
|
|| operation.ProjectionSequence != 0UL
|
||||||
|
|| operation.CollisionGeneration != _physics
|
||||||
|
.ExpectedCollisionGeneration(operation.ExactCellId)
|
||||||
|
|| !_physics.Engine.IsSpawnCellReady(operation.ExactCellId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UnindexDeferred(operation);
|
||||||
|
operation.WakeableLostCell = false;
|
||||||
|
operation.CollisionGenerationReady = false;
|
||||||
|
operation.Stage = RuntimeEntityPlacementStage.AwaitingPreparation;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool TryPrepareDormantLocalActivationCommit(
|
||||||
|
RuntimeEntityRecord record,
|
||||||
|
PhysicsBody body,
|
||||||
|
in RuntimeDormantSetPositionEvaluation evaluation,
|
||||||
|
bool provenShapeless,
|
||||||
|
out PreparedDormantSetPositionCommit? prepared)
|
||||||
|
{
|
||||||
|
EnsureNotDisposed();
|
||||||
|
ArgumentNullException.ThrowIfNull(record);
|
||||||
|
ArgumentNullException.ThrowIfNull(body);
|
||||||
|
prepared = null;
|
||||||
|
if (!IsDormantLocalEvaluationCurrent(record, body, evaluation)
|
||||||
|
|| !IsExactDormantLocalActivationCurrent(
|
||||||
|
record,
|
||||||
|
body,
|
||||||
|
evaluation.Placement,
|
||||||
|
evaluation.Command,
|
||||||
|
out Operation? operation,
|
||||||
|
allowCanonicalCommand: true)
|
||||||
|
|| operation is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PhysicsSetPositionResult result = evaluation.Result;
|
||||||
|
ShadowObjectRegistry.PreparedSetPositionShadowCommit? shadow = null;
|
||||||
|
RuntimeCollisionReportingState.PreparedSetPositionCollisionBatch?
|
||||||
|
collision = null;
|
||||||
|
RuntimePlacementProjectionSnapshot projection = default;
|
||||||
|
SortedDictionary<ulong, RuntimePlacementProjectionSnapshot>?
|
||||||
|
pendingProjection = null;
|
||||||
|
ulong deferredCollisionGeneration = 0UL;
|
||||||
|
List<RuntimeEntityKey>? deferredBucket = null;
|
||||||
|
bool deferredBucketIsNew = false;
|
||||||
|
|
||||||
|
if (!result.IsDeferred)
|
||||||
|
{
|
||||||
|
if (!_physics.CollisionReports.TryPrepareSetPositionBatch(
|
||||||
|
record,
|
||||||
|
body,
|
||||||
|
evaluation.Command.GameTime,
|
||||||
|
result.IsCommitted && operation.PreviousContact,
|
||||||
|
result.IsCommitted && operation.PreviousOnWalkable,
|
||||||
|
result.IsCommitted && result.OnWalkable,
|
||||||
|
result.CollidedWithEnvironment,
|
||||||
|
result.CollidedObjectIds,
|
||||||
|
out collision)
|
||||||
|
|| collision is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.IsDeferred)
|
||||||
|
{
|
||||||
|
deferredCollisionGeneration = _physics
|
||||||
|
.ExpectedCollisionGeneration(result.CellId);
|
||||||
|
_preparedMovers.EnsureCapacity(_preparedMovers.Count + 1);
|
||||||
|
if (result.CellId != 0u && deferredCollisionGeneration != 0UL)
|
||||||
|
{
|
||||||
|
var bucketKey = new CellGenerationKey(
|
||||||
|
result.CellId,
|
||||||
|
deferredCollisionGeneration);
|
||||||
|
if (_deferredByCellGeneration.TryGetValue(
|
||||||
|
bucketKey,
|
||||||
|
out deferredBucket))
|
||||||
|
{
|
||||||
|
deferredBucket.EnsureCapacity(deferredBucket.Count + 1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
deferredBucket = [operation.Key];
|
||||||
|
deferredBucketIsNew = true;
|
||||||
|
_deferredByCellGeneration.EnsureCapacity(
|
||||||
|
_deferredByCellGeneration.Count + 1);
|
||||||
|
_deferredBucketOrder.EnsureCapacity(
|
||||||
|
_deferredBucketOrder.Count + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!_physics.Engine.ShadowObjects.TryPrepareSetPosition(
|
||||||
|
operation.Key.LocalEntityId,
|
||||||
|
result.Position,
|
||||||
|
result.Orientation,
|
||||||
|
result.CellId,
|
||||||
|
evaluation.Command.ShadowWorldOffsetX,
|
||||||
|
evaluation.Command.ShadowWorldOffsetY,
|
||||||
|
PhysicsShadowCommitAction.Preserve,
|
||||||
|
ImmutableArray<uint>.Empty,
|
||||||
|
provenShapeless,
|
||||||
|
suspendOwner: true,
|
||||||
|
out shadow)
|
||||||
|
|| shadow is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong expectedProjectionSequence = _nextProjectionSequence;
|
||||||
|
if (result.IsCommitted)
|
||||||
|
{
|
||||||
|
_ = checked(record.ObjectClockEpoch + 1UL);
|
||||||
|
_ = checked(record.PlacementCommitVersion + 1UL);
|
||||||
|
if (record.FullCellId != result.CellId)
|
||||||
|
_ = checked(record.SpatialAuthorityVersion + 1UL);
|
||||||
|
if (!_physics.TryPrepareSpatialRootAdmission(record))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ulong sequence = checked(expectedProjectionSequence + 1UL);
|
||||||
|
ulong spatial = record.SpatialAuthorityVersion
|
||||||
|
+ (record.FullCellId == result.CellId ? 0UL : 1UL);
|
||||||
|
ulong placement = checked(record.PlacementCommitVersion + 1UL);
|
||||||
|
var token = new RuntimePlacementProjectionToken(
|
||||||
|
sequence,
|
||||||
|
Revision: 1UL,
|
||||||
|
operation.Key,
|
||||||
|
operation.PositionAuthorityVersion,
|
||||||
|
spatial,
|
||||||
|
placement,
|
||||||
|
operation.SessionLifetimeVersion,
|
||||||
|
result.CellId,
|
||||||
|
operation.CollisionGeneration,
|
||||||
|
evaluation.Command.Portal);
|
||||||
|
projection = new RuntimePlacementProjectionSnapshot(
|
||||||
|
token,
|
||||||
|
RuntimePlacementProjectionKind.Place,
|
||||||
|
result.Position,
|
||||||
|
result.Orientation,
|
||||||
|
result.CellLocalPosition,
|
||||||
|
result.InContact,
|
||||||
|
result.OnWalkable);
|
||||||
|
pendingProjection = new SortedDictionary<
|
||||||
|
ulong,
|
||||||
|
RuntimePlacementProjectionSnapshot>(_pendingProjection)
|
||||||
|
{
|
||||||
|
[sequence] = projection,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
prepared = new PreparedDormantSetPositionCommit
|
||||||
|
{
|
||||||
|
Evaluation = evaluation,
|
||||||
|
Entity = operation.Key,
|
||||||
|
OperationId = operation.Token.OperationId,
|
||||||
|
ExpectedProjectionSequence = expectedProjectionSequence,
|
||||||
|
Shadow = shadow,
|
||||||
|
Collision = collision,
|
||||||
|
Projection = projection,
|
||||||
|
PendingProjection = pendingProjection,
|
||||||
|
DeferredCollisionGeneration = deferredCollisionGeneration,
|
||||||
|
DeferredBucket = deferredBucket,
|
||||||
|
DeferredBucketIsNew = deferredBucketIsNew,
|
||||||
|
};
|
||||||
|
return IsPreparedDormantCommitCurrent(record, body, prepared);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool TryApplyDormantLocalActivationCommit(
|
||||||
|
RuntimeEntityRecord record,
|
||||||
|
PhysicsBody body,
|
||||||
|
PlayerMovementController controller,
|
||||||
|
EntityPhysicsHost physicsHost,
|
||||||
|
PreparedDormantSetPositionCommit prepared,
|
||||||
|
out RuntimeDormantSetPositionCommitReceipt receipt)
|
||||||
|
{
|
||||||
|
EnsureNotDisposed();
|
||||||
|
ArgumentNullException.ThrowIfNull(record);
|
||||||
|
ArgumentNullException.ThrowIfNull(body);
|
||||||
|
ArgumentNullException.ThrowIfNull(controller);
|
||||||
|
ArgumentNullException.ThrowIfNull(physicsHost);
|
||||||
|
ArgumentNullException.ThrowIfNull(prepared);
|
||||||
|
receipt = default;
|
||||||
|
if (!IsPreparedDormantCommitCurrent(record, body, prepared)
|
||||||
|
|| !_operations.TryGetValue(
|
||||||
|
prepared.Entity,
|
||||||
|
out Operation? operation))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PhysicsSetPositionResult result = prepared.Evaluation.Result;
|
||||||
|
operation.Body = body;
|
||||||
|
operation.DormantLocalActivation = true;
|
||||||
|
if (result.IsDeferred)
|
||||||
|
{
|
||||||
|
if (prepared.Shadow is null
|
||||||
|
|| !_physics.Engine.ShadowObjects.TryApplySetPosition(
|
||||||
|
prepared.Shadow,
|
||||||
|
out ShadowObjectRegistry.SetPositionShadowCommitReceipt
|
||||||
|
deferredShadowReceipt))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
body.Orientation = result.Orientation;
|
||||||
|
body.StageDormantCellFrame(
|
||||||
|
result.CellId,
|
||||||
|
result.Position,
|
||||||
|
result.CellLocalPosition);
|
||||||
|
body.InWorld = false;
|
||||||
|
body.TransientState &= ~TransientStateFlags.Active;
|
||||||
|
operation.Result = result;
|
||||||
|
operation.ExactCellId = result.CellId;
|
||||||
|
operation.WakeableLostCell = true;
|
||||||
|
operation.CollisionGeneration = prepared
|
||||||
|
.DeferredCollisionGeneration;
|
||||||
|
operation.CollisionGenerationReady = false;
|
||||||
|
operation.Stage = RuntimeEntityPlacementStage.AwaitingCell;
|
||||||
|
_preparedMovers[operation.Key] = prepared.Evaluation.Command.Physics;
|
||||||
|
if (prepared.DeferredBucket is { } deferredBucket)
|
||||||
|
{
|
||||||
|
var bucketKey = new CellGenerationKey(
|
||||||
|
result.CellId,
|
||||||
|
prepared.DeferredCollisionGeneration);
|
||||||
|
if (prepared.DeferredBucketIsNew)
|
||||||
|
{
|
||||||
|
_deferredByCellGeneration.Add(bucketKey, deferredBucket);
|
||||||
|
_deferredBucketOrder.Add(bucketKey);
|
||||||
|
}
|
||||||
|
else if (!deferredBucket.Contains(operation.Key))
|
||||||
|
{
|
||||||
|
deferredBucket.Add(operation.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
receipt = new RuntimeDormantSetPositionCommitReceipt(
|
||||||
|
RuntimeDormantSetPositionCommitStatus.DeferredCell,
|
||||||
|
operation.Key,
|
||||||
|
operation.Token.OperationId,
|
||||||
|
default,
|
||||||
|
default,
|
||||||
|
deferredShadowReceipt,
|
||||||
|
prepared.Evaluation.CollisionAuthority,
|
||||||
|
operation.Record.VectorAuthorityVersion,
|
||||||
|
HitGround: false,
|
||||||
|
LeaveGround: false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prepared.Collision is null
|
||||||
|
|| !_physics.CollisionReports.TryInstallSetPositionBatch(
|
||||||
|
prepared.Collision,
|
||||||
|
out RuntimeCollisionReportingState
|
||||||
|
.SetPositionCollisionBatchReceipt collisionReceipt))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.IsCommitted)
|
||||||
|
{
|
||||||
|
operation.Result = result;
|
||||||
|
receipt = new RuntimeDormantSetPositionCommitReceipt(
|
||||||
|
RuntimeDormantSetPositionCommitStatus.RejectedPlacement,
|
||||||
|
operation.Key,
|
||||||
|
operation.Token.OperationId,
|
||||||
|
default,
|
||||||
|
collisionReceipt,
|
||||||
|
default,
|
||||||
|
prepared.Evaluation.CollisionAuthority,
|
||||||
|
operation.Record.VectorAuthorityVersion,
|
||||||
|
HitGround: false,
|
||||||
|
LeaveGround: false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool previousOnWalkable = operation.PreviousOnWalkable;
|
||||||
|
bool hitGround = !previousOnWalkable
|
||||||
|
&& result.InContact
|
||||||
|
&& result.OnWalkable;
|
||||||
|
bool leaveGround = previousOnWalkable
|
||||||
|
&& !(result.InContact && result.OnWalkable);
|
||||||
|
|
||||||
|
UnindexDeferred(operation);
|
||||||
|
body.Orientation = result.Orientation;
|
||||||
|
body.StageDormantCellFrame(
|
||||||
|
result.CellId,
|
||||||
|
result.Position,
|
||||||
|
result.CellLocalPosition);
|
||||||
|
body.LastUpdateTime = prepared.Evaluation.Command.GameTime;
|
||||||
|
body.ContactPlaneValid = result.InContact;
|
||||||
|
body.ContactPlane = result.ContactPlane;
|
||||||
|
body.ContactPlaneCellId = result.ContactPlaneCellId;
|
||||||
|
body.ContactPlaneIsWater = result.ContactPlaneIsWater;
|
||||||
|
if (result.InContact)
|
||||||
|
body.GroundNormal = result.ContactPlane.Normal;
|
||||||
|
_ = PhysicsObjUpdate.CommitSetPositionContactPrefix(
|
||||||
|
body,
|
||||||
|
result.InContact,
|
||||||
|
result.OnWalkable,
|
||||||
|
previousOnWalkable);
|
||||||
|
operation.Result = result;
|
||||||
|
operation.ExactCellId = result.CellId;
|
||||||
|
operation.WakeableLostCell = false;
|
||||||
|
operation.CollisionGenerationReady = false;
|
||||||
|
operation.EnteringWorldFromCelllessResidence = false;
|
||||||
|
operation.Stage = RuntimeEntityPlacementStage
|
||||||
|
.AwaitingFinalShadowPreparation;
|
||||||
|
receipt = new RuntimeDormantSetPositionCommitReceipt(
|
||||||
|
RuntimeDormantSetPositionCommitStatus
|
||||||
|
.AwaitingFinalShadowPreparation,
|
||||||
|
operation.Key,
|
||||||
|
operation.Token.OperationId,
|
||||||
|
prepared.Projection,
|
||||||
|
collisionReceipt,
|
||||||
|
default,
|
||||||
|
prepared.Evaluation.CollisionAuthority,
|
||||||
|
operation.Record.VectorAuthorityVersion,
|
||||||
|
hitGround,
|
||||||
|
leaveGround);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal SetPositionCollisionBatchDispatchResult
|
||||||
|
DispatchDormantLocalActivationCollision(
|
||||||
|
in RuntimeDormantSetPositionCommitReceipt receipt)
|
||||||
|
{
|
||||||
|
if (receipt.Status
|
||||||
|
is RuntimeDormantSetPositionCommitStatus.DeferredCell
|
||||||
|
or RuntimeDormantSetPositionCommitStatus.RejectedAuthority)
|
||||||
|
{
|
||||||
|
return new(
|
||||||
|
SetPositionCollisionBatchDispatchStatus.RejectedReceipt,
|
||||||
|
Reported: false);
|
||||||
|
}
|
||||||
|
SetPositionCollisionBatchDispatchResult dispatch = _physics
|
||||||
|
.CollisionReports.DispatchSetPositionBatchResult(receipt.Collision);
|
||||||
|
bool reported = dispatch.Reported;
|
||||||
|
if (receipt.Status
|
||||||
|
is RuntimeDormantSetPositionCommitStatus.RejectedPlacement
|
||||||
|
&& _operations.TryGetValue(receipt.Entity, out Operation? operation)
|
||||||
|
&& operation.Token.OperationId == receipt.OperationId
|
||||||
|
&& IsCurrent(operation)
|
||||||
|
&& !operation.Result.IsCommitted
|
||||||
|
&& !operation.Result.IsDeferred)
|
||||||
|
{
|
||||||
|
operation.Result = operation.Result with
|
||||||
|
{
|
||||||
|
Error = reported
|
||||||
|
? PhysicsSetPositionError.Collided
|
||||||
|
: PhysicsSetPositionError.NoValidPosition,
|
||||||
|
CollisionHandlerResult = reported,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return dispatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool IsDormantLocalActivationPrephaseCurrent(
|
||||||
|
RuntimeEntityRecord record,
|
||||||
|
PhysicsBody body,
|
||||||
|
in RuntimeDormantSetPositionCommitReceipt receipt)
|
||||||
|
{
|
||||||
|
return receipt.Status is RuntimeDormantSetPositionCommitStatus
|
||||||
|
.AwaitingFinalShadowPreparation
|
||||||
|
&& _operations.TryGetValue(receipt.Entity, out Operation? operation)
|
||||||
|
&& operation.Token.OperationId == receipt.OperationId
|
||||||
|
&& operation.Stage is RuntimeEntityPlacementStage
|
||||||
|
.AwaitingFinalShadowPreparation
|
||||||
|
&& operation.DormantLocalActivation
|
||||||
|
&& IsCurrent(operation)
|
||||||
|
&& ReferenceEquals(operation.Record, record)
|
||||||
|
&& ReferenceEquals(record.PhysicsBody, body)
|
||||||
|
&& !body.InWorld
|
||||||
|
&& (body.TransientState & TransientStateFlags.Active) == 0
|
||||||
|
&& record.PhysicsHost is null
|
||||||
|
&& record.RemoteMotion is null
|
||||||
|
&& record.Projectile is null
|
||||||
|
&& !_physics.IsSpatialRoot(record)
|
||||||
|
&& _moverPreparationAuthorities.TryGetValue(
|
||||||
|
receipt.Entity,
|
||||||
|
out MoverPreparationAuthority authority)
|
||||||
|
&& authority.OperationId == receipt.OperationId
|
||||||
|
&& authority.Prepared
|
||||||
|
&& record.PositionAuthorityVersion
|
||||||
|
== authority.PositionAuthorityVersion
|
||||||
|
&& record.ObjDescAuthorityVersion
|
||||||
|
== authority.ObjDescAuthorityVersion
|
||||||
|
&& record.CreateIntegrationVersion
|
||||||
|
== authority.CreateIntegrationVersion
|
||||||
|
&& CanonicalSetupTableId(record) == authority.SetupTableId;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool IsDormantLocalActivationResponseCurrent(
|
||||||
|
RuntimeEntityRecord record,
|
||||||
|
PhysicsBody body,
|
||||||
|
in RuntimeDormantSetPositionCommitReceipt receipt)
|
||||||
|
{
|
||||||
|
if (receipt.Status is RuntimeDormantSetPositionCommitStatus
|
||||||
|
.AwaitingFinalShadowPreparation)
|
||||||
|
return IsDormantLocalActivationPrephaseCurrent(record, body, receipt);
|
||||||
|
return receipt.Status is RuntimeDormantSetPositionCommitStatus
|
||||||
|
.RejectedPlacement
|
||||||
|
&& _operations.TryGetValue(receipt.Entity, out Operation? operation)
|
||||||
|
&& operation.Token.OperationId == receipt.OperationId
|
||||||
|
&& operation.DormantLocalActivation
|
||||||
|
&& IsCurrent(operation)
|
||||||
|
&& ReferenceEquals(operation.Record, record)
|
||||||
|
&& ReferenceEquals(record.PhysicsBody, body)
|
||||||
|
&& !operation.Result.IsCommitted
|
||||||
|
&& !operation.Result.IsDeferred
|
||||||
|
&& !body.InWorld
|
||||||
|
&& (body.TransientState & TransientStateFlags.Active) == 0
|
||||||
|
&& record.PhysicsHost is null
|
||||||
|
&& record.RemoteMotion is null
|
||||||
|
&& record.Projectile is null
|
||||||
|
&& !_physics.IsSpatialRoot(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool CommitDormantLocalActivationPostGround(
|
||||||
|
RuntimeEntityRecord record,
|
||||||
|
PhysicsBody body,
|
||||||
|
in RuntimeDormantSetPositionCommitReceipt receipt)
|
||||||
|
{
|
||||||
|
if (!IsDormantLocalActivationPrephaseCurrent(record, body, receipt))
|
||||||
|
return false;
|
||||||
|
PhysicsObjUpdate.CommitSetPositionPostGround(body);
|
||||||
|
PhysicsSetPositionResult result = _operations[receipt.Entity].Result;
|
||||||
|
body.SlidingNormal = result.SlidingNormal;
|
||||||
|
if (result.SlidingNormalValid)
|
||||||
|
body.TransientState |= TransientStateFlags.Sliding;
|
||||||
|
else
|
||||||
|
body.TransientState &= ~TransientStateFlags.Sliding;
|
||||||
|
return IsDormantLocalActivationPrephaseCurrent(record, body, receipt);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool CommitDormantLocalActivationPostCollision(
|
||||||
|
RuntimeEntityRecord record,
|
||||||
|
PhysicsBody body,
|
||||||
|
in RuntimeDormantSetPositionCommitReceipt receipt)
|
||||||
|
{
|
||||||
|
if (!_operations.TryGetValue(receipt.Entity, out Operation? operation)
|
||||||
|
|| operation.Token.OperationId != receipt.OperationId
|
||||||
|
|| !IsCurrent(operation)
|
||||||
|
|| !ReferenceEquals(operation.Record, record)
|
||||||
|
|| !ReferenceEquals(record.PhysicsBody, body))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (receipt.Status is RuntimeDormantSetPositionCommitStatus
|
||||||
|
.AwaitingFinalShadowPreparation
|
||||||
|
&& !IsDormantLocalActivationPrephaseCurrent(record, body, receipt))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
PhysicsSetPositionResult result = operation.Result;
|
||||||
|
body.FramesStationaryFall = result.FramesStationaryFall;
|
||||||
|
if (IsVelocityCurrent(operation))
|
||||||
|
{
|
||||||
|
PhysicsObjUpdate.HandleAllCollisions(
|
||||||
|
body,
|
||||||
|
result.CollisionNormalValid,
|
||||||
|
result.CollisionNormal,
|
||||||
|
operation.PreviousContact,
|
||||||
|
operation.PreviousOnWalkable,
|
||||||
|
body.OnWalkable);
|
||||||
|
}
|
||||||
|
CommitStationaryBits(body, result.FramesStationaryFall);
|
||||||
|
return IsCurrent(operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool TryPrepareDormantLocalActivationFinalCommit(
|
||||||
|
RuntimeEntityRecord record,
|
||||||
|
PhysicsBody body,
|
||||||
|
in RuntimeDormantSetPositionCommitReceipt receipt,
|
||||||
|
bool provenShapeless,
|
||||||
|
out PreparedDormantActivationFinalCommit? prepared)
|
||||||
|
{
|
||||||
|
prepared = null;
|
||||||
|
if (!IsDormantLocalActivationPrephaseCurrent(record, body, receipt)
|
||||||
|
|| !_operations.TryGetValue(receipt.Entity, out Operation? operation))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
PhysicsSetPositionResult result = operation.Result;
|
||||||
|
if (!_physics.Engine.ShadowObjects.TryPrepareSetPosition(
|
||||||
|
operation.Key.LocalEntityId,
|
||||||
|
result.Position,
|
||||||
|
result.Orientation,
|
||||||
|
result.CellId,
|
||||||
|
operation.Command.ShadowWorldOffsetX,
|
||||||
|
operation.Command.ShadowWorldOffsetY,
|
||||||
|
result.ShadowAction,
|
||||||
|
result.CrossCellIds,
|
||||||
|
provenShapeless,
|
||||||
|
suspendOwner: false,
|
||||||
|
out ShadowObjectRegistry.PreparedSetPositionShadowCommit? shadow)
|
||||||
|
|| shadow is null
|
||||||
|
|| _nextProjectionSequence + 1UL
|
||||||
|
!= receipt.Projection.Token.Sequence)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var pending = new SortedDictionary<
|
||||||
|
ulong,
|
||||||
|
RuntimePlacementProjectionSnapshot>(_pendingProjection)
|
||||||
|
{
|
||||||
|
[receipt.Projection.Token.Sequence] = receipt.Projection,
|
||||||
|
};
|
||||||
|
prepared = new PreparedDormantActivationFinalCommit
|
||||||
|
{
|
||||||
|
Entity = receipt.Entity,
|
||||||
|
OperationId = receipt.OperationId,
|
||||||
|
ExpectedProjectionSequence = _nextProjectionSequence,
|
||||||
|
Shadow = shadow,
|
||||||
|
Projection = receipt.Projection,
|
||||||
|
PendingProjection = pending,
|
||||||
|
};
|
||||||
|
bool current = IsDormantLocalActivationPrephaseCurrent(
|
||||||
|
record, body, receipt);
|
||||||
|
bool shadowCurrent = _physics.Engine.ShadowObjects
|
||||||
|
.IsPreparedSetPositionCurrent(shadow);
|
||||||
|
return current && shadowCurrent;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool TryApplyDormantLocalActivationFinalCommit(
|
||||||
|
RuntimeEntityRecord record,
|
||||||
|
PhysicsBody body,
|
||||||
|
PlayerMovementController controller,
|
||||||
|
EntityPhysicsHost physicsHost,
|
||||||
|
in RuntimeDormantSetPositionCommitReceipt prephase,
|
||||||
|
PreparedDormantActivationFinalCommit prepared,
|
||||||
|
out RuntimeDormantSetPositionCommitReceipt committed)
|
||||||
|
{
|
||||||
|
committed = default;
|
||||||
|
if (prepared.Entity != prephase.Entity
|
||||||
|
|| prepared.OperationId != prephase.OperationId
|
||||||
|
|| prepared.ExpectedProjectionSequence != _nextProjectionSequence
|
||||||
|
|| prepared.Projection != prephase.Projection
|
||||||
|
|| !IsDormantLocalActivationPrephaseCurrent(record, body, prephase)
|
||||||
|
|| !_physics.Engine.ShadowObjects.TryApplySetPosition(
|
||||||
|
prepared.Shadow,
|
||||||
|
out ShadowObjectRegistry.SetPositionShadowCommitReceipt shadow))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Operation operation = _operations[prephase.Entity];
|
||||||
|
PhysicsSetPositionResult result = operation.Result;
|
||||||
|
if (record.FullCellId != result.CellId)
|
||||||
|
{
|
||||||
|
_entities.SetFullCell(record, result.CellId,
|
||||||
|
(result.CellId & 0xFFFF0000u) | 0xFFFFu);
|
||||||
|
}
|
||||||
|
operation.SpatialAuthorityVersion = record.SpatialAuthorityVersion;
|
||||||
|
_entities.AdvancePlacementCommit(record);
|
||||||
|
operation.PlacementCommitVersion = record.PlacementCommitVersion;
|
||||||
|
body.InWorld = true;
|
||||||
|
bool isStatic = (record.FinalPhysicsState & PhysicsStateFlags.Static) != 0;
|
||||||
|
if (!isStatic)
|
||||||
|
body.TransientState |= TransientStateFlags.Active;
|
||||||
|
_entities.SetPhysicsHost(record, physicsHost);
|
||||||
|
controller.CommitRuntimeActivationFrame();
|
||||||
|
_physics.Engine.UpdatePlayerCurrCell(result.CellId);
|
||||||
|
_physics.AcknowledgeSpatialProjection(record, spatial: true);
|
||||||
|
_entities.ResetObjectClockForEnterWorld(record, isStatic);
|
||||||
|
operation.Stage = RuntimeEntityPlacementStage.AwaitingCommitAcknowledgement;
|
||||||
|
operation.ProjectionSequence = prepared.Projection.Token.Sequence;
|
||||||
|
_pendingProjection = prepared.PendingProjection;
|
||||||
|
_nextProjectionSequence = prepared.Projection.Token.Sequence;
|
||||||
|
CancelLostFamilyDeadlines(operation);
|
||||||
|
controller.ActivateRuntimePublication();
|
||||||
|
committed = prephase with
|
||||||
|
{
|
||||||
|
Status = RuntimeDormantSetPositionCommitStatus.Committed,
|
||||||
|
Shadow = shadow,
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void DispatchDormantLocalActivationShadow(
|
||||||
|
in RuntimeDormantSetPositionCommitReceipt receipt)
|
||||||
|
{
|
||||||
|
if (receipt.Status is not (
|
||||||
|
RuntimeDormantSetPositionCommitStatus.Committed
|
||||||
|
or RuntimeDormantSetPositionCommitStatus.DeferredCell))
|
||||||
|
return;
|
||||||
|
_physics.Engine.ShadowObjects.DispatchSetPositionCommit(receipt.Shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void DispatchDormantLocalActivationPlacement(
|
||||||
|
in RuntimeDormantSetPositionCommitReceipt receipt)
|
||||||
|
{
|
||||||
|
if (!receipt.IsCommitted)
|
||||||
|
return;
|
||||||
|
PublishPlacement(receipt.Projection);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void DiscardDormantLocalActivationDispatches(
|
||||||
|
in RuntimeDormantSetPositionCommitReceipt receipt,
|
||||||
|
bool collisionAlreadyDispatched)
|
||||||
|
{
|
||||||
|
if (!collisionAlreadyDispatched)
|
||||||
|
_physics.CollisionReports.DiscardSetPositionBatch(receipt.Collision);
|
||||||
|
_physics.Engine.ShadowObjects.DiscardSetPositionCommit(receipt.Shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void RetireDormantLocalActivation(
|
||||||
|
in RuntimeDormantSetPositionCommitReceipt receipt,
|
||||||
|
bool collisionAlreadyDispatched)
|
||||||
|
{
|
||||||
|
DiscardDormantLocalActivationDispatches(
|
||||||
|
receipt,
|
||||||
|
collisionAlreadyDispatched);
|
||||||
|
_physics.CollisionReports.RetireSetPositionBatchOwner(
|
||||||
|
receipt.Collision);
|
||||||
|
if (_operations.TryGetValue(receipt.Entity, out Operation? operation)
|
||||||
|
&& operation.Token.OperationId == receipt.OperationId)
|
||||||
|
{
|
||||||
|
_ = CancelCore(operation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void RetireDormantLocalActivationToken(
|
||||||
|
RuntimeEntityRecord record,
|
||||||
|
in RuntimeEntityPlacementToken token)
|
||||||
|
{
|
||||||
|
if (!token.IsValid
|
||||||
|
|| record.Key != token.Entity
|
||||||
|
|| !_operations.TryGetValue(token.Entity, out Operation? operation)
|
||||||
|
|| operation.Token != token
|
||||||
|
|| !ReferenceEquals(operation.Record, record))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ = CancelCore(operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool IsDormantLocalActivationCommitCurrent(
|
||||||
|
RuntimeEntityRecord record,
|
||||||
|
PhysicsBody body,
|
||||||
|
in RuntimeDormantSetPositionCommitReceipt receipt)
|
||||||
|
{
|
||||||
|
if (!receipt.IsCommitted
|
||||||
|
|| !receipt.Projection.Token.IsValid
|
||||||
|
|| record.Key != receipt.Projection.Token.Entity
|
||||||
|
|| !ReferenceEquals(record.PhysicsBody, body)
|
||||||
|
|| !_pendingProjection.TryGetValue(
|
||||||
|
receipt.Projection.Token.Sequence,
|
||||||
|
out RuntimePlacementProjectionSnapshot pending)
|
||||||
|
|| pending != receipt.Projection
|
||||||
|
|| !_operations.TryGetValue(
|
||||||
|
receipt.Projection.Token.Entity,
|
||||||
|
out Operation? operation)
|
||||||
|
|| operation.Stage is not RuntimeEntityPlacementStage
|
||||||
|
.AwaitingCommitAcknowledgement
|
||||||
|
|| operation.ProjectionSequence
|
||||||
|
!= receipt.Projection.Token.Sequence
|
||||||
|
|| !IsCurrent(operation)
|
||||||
|
|| !body.InWorld
|
||||||
|
|| !_physics.IsSpatialRoot(record))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool TryCaptureDormantLocalActivationResult(
|
||||||
|
in RuntimeEntityPlacementToken token,
|
||||||
|
out PhysicsSetPositionResult result)
|
||||||
|
{
|
||||||
|
if (!_disposed
|
||||||
|
&& token.IsValid
|
||||||
|
&& _operations.TryGetValue(token.Entity, out Operation? operation)
|
||||||
|
&& operation.Token == token)
|
||||||
|
{
|
||||||
|
result = operation.Result;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsPreparedDormantCommitCurrent(
|
||||||
|
RuntimeEntityRecord record,
|
||||||
|
PhysicsBody body,
|
||||||
|
PreparedDormantSetPositionCommit prepared)
|
||||||
|
{
|
||||||
|
if (_nextProjectionSequence != prepared.ExpectedProjectionSequence
|
||||||
|
|| !IsDormantLocalEvaluationCurrent(
|
||||||
|
record,
|
||||||
|
body,
|
||||||
|
prepared.Evaluation)
|
||||||
|
|| !_operations.TryGetValue(
|
||||||
|
prepared.Entity,
|
||||||
|
out Operation? operation)
|
||||||
|
|| operation.Token.OperationId != prepared.OperationId)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (prepared.Collision is not null
|
||||||
|
&& !_physics.CollisionReports.IsPreparedSetPositionBatchCurrent(
|
||||||
|
prepared.Collision))
|
||||||
|
return false;
|
||||||
|
return prepared.Shadow is null
|
||||||
|
|| _physics.Engine.ShadowObjects.IsPreparedSetPositionCurrent(
|
||||||
|
prepared.Shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CommitStationaryBits(
|
||||||
|
PhysicsBody body,
|
||||||
|
int framesStationaryFall)
|
||||||
|
{
|
||||||
|
body.TransientState &= ~(TransientStateFlags.StationaryFall
|
||||||
|
| TransientStateFlags.StationaryStop
|
||||||
|
| TransientStateFlags.StationaryStuck);
|
||||||
|
body.TransientState |= framesStationaryFall switch
|
||||||
|
{
|
||||||
|
1 => TransientStateFlags.StationaryFall,
|
||||||
|
2 => TransientStateFlags.StationaryStop,
|
||||||
|
3 => TransientStateFlags.StationaryStuck,
|
||||||
|
_ => TransientStateFlags.None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
internal RuntimeSetPositionOutcome SubmitPreparedPlacement(
|
internal RuntimeSetPositionOutcome SubmitPreparedPlacement(
|
||||||
in RuntimeEntityPlacementToken token,
|
in RuntimeEntityPlacementToken token,
|
||||||
in RuntimeSetPositionCommand command) =>
|
in RuntimeSetPositionCommand command) =>
|
||||||
|
|
@ -1559,6 +2412,12 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
|
|
||||||
private void RetryDeferred(Operation operation)
|
private void RetryDeferred(Operation operation)
|
||||||
{
|
{
|
||||||
|
// The local-player activation lease owns its dormant body/controller
|
||||||
|
// and must re-enter through the same sealed evaluation/commit path.
|
||||||
|
// A collision-generation wake only marks readiness; it must never
|
||||||
|
// bypass that path through the ordinary remote CommitCanonical tail.
|
||||||
|
if (operation.DormantLocalActivation)
|
||||||
|
return;
|
||||||
if (!IsCurrent(operation)
|
if (!IsCurrent(operation)
|
||||||
|| !operation.WakeableLostCell
|
|| !operation.WakeableLostCell
|
||||||
|| operation.RequiresPreparation
|
|| operation.RequiresPreparation
|
||||||
|
|
@ -2006,7 +2865,8 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
in RuntimeEntityPlacementToken token,
|
in RuntimeEntityPlacementToken token,
|
||||||
in RuntimeSetPositionCommand command,
|
in RuntimeSetPositionCommand command,
|
||||||
out Operation? operation,
|
out Operation? operation,
|
||||||
bool allowCanonicalCommand = false)
|
bool allowCanonicalCommand = false,
|
||||||
|
bool allowDeferredLease = false)
|
||||||
{
|
{
|
||||||
operation = null;
|
operation = null;
|
||||||
if (!token.IsValid
|
if (!token.IsValid
|
||||||
|
|
@ -2021,6 +2881,14 @@ internal sealed class RuntimeSetPositionState : IDisposable
|
||||||
|| operation.Token != token
|
|| operation.Token != token
|
||||||
|| operation.Stage
|
|| operation.Stage
|
||||||
is not RuntimeEntityPlacementStage.AwaitingPreparation
|
is not RuntimeEntityPlacementStage.AwaitingPreparation
|
||||||
|
&& !(allowDeferredLease
|
||||||
|
&& operation.DormantLocalActivation
|
||||||
|
&& (operation.Stage
|
||||||
|
is RuntimeEntityPlacementStage.AwaitingCell
|
||||||
|
&& operation.WakeableLostCell
|
||||||
|
|| operation.Stage is RuntimeEntityPlacementStage
|
||||||
|
.AwaitingFinalShadowPreparation)
|
||||||
|
&& operation.ProjectionSequence == 0UL)
|
||||||
|| !ReferenceEquals(operation.Record, record)
|
|| !ReferenceEquals(operation.Record, record)
|
||||||
|| !IsCurrent(operation)
|
|| !IsCurrent(operation)
|
||||||
|| !ReferenceEquals(record.PhysicsBody, body)
|
|| !ReferenceEquals(record.PhysicsBody, body)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,284 @@ public sealed class ShadowSetPositionCommitTests
|
||||||
private const uint Cell1 = Landblock | 0x0001u;
|
private const uint Cell1 = Landblock | 0x0001u;
|
||||||
private const uint Cell9 = Landblock | 0x0009u;
|
private const uint Cell9 = Landblock | 0x0009u;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreparedAuthoredMoveKeepsOldRowsUntilAtomicApply()
|
||||||
|
{
|
||||||
|
var registry = RegisteredSingle();
|
||||||
|
var moved = new Vector3(36f, 12f, 50f);
|
||||||
|
Assert.True(registry.TryPrepareSetPosition(
|
||||||
|
1u,
|
||||||
|
moved,
|
||||||
|
Quaternion.Identity,
|
||||||
|
Cell9,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
PhysicsShadowCommitAction.Replace,
|
||||||
|
[Cell9],
|
||||||
|
provenShapeless: false,
|
||||||
|
suspendOwner: false,
|
||||||
|
out var prepared));
|
||||||
|
|
||||||
|
Assert.Equal(new Vector3(12f, 12f, 50f),
|
||||||
|
Assert.Single(registry.GetObjectsInCell(Cell1)).Position);
|
||||||
|
Assert.Empty(registry.GetObjectsInCell(Cell9));
|
||||||
|
Assert.True(registry.TryApplySetPosition(prepared!, out var receipt));
|
||||||
|
Assert.True(receipt.Mutated);
|
||||||
|
Assert.Empty(registry.GetObjectsInCell(Cell1));
|
||||||
|
Assert.Equal(moved,
|
||||||
|
Assert.Single(registry.GetObjectsInCell(Cell9)).Position);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreparedCrossPrefixDispatchesMembershipThenMutationOnce()
|
||||||
|
{
|
||||||
|
const uint otherCell = 0xA9B50001u;
|
||||||
|
var registry = RegisteredSingle();
|
||||||
|
var callbacks = new List<string>();
|
||||||
|
registry.OwnerPrefixMembershipChanged += (owner, prefix) =>
|
||||||
|
callbacks.Add($"prefix:{owner:X8}:{prefix:X8}");
|
||||||
|
registry.OwnerMutated += (owner, version) =>
|
||||||
|
callbacks.Add($"owner:{owner:X8}:{version}");
|
||||||
|
Assert.True(registry.TryPrepareSetPosition(
|
||||||
|
1u,
|
||||||
|
new Vector3(204f, 12f, 50f),
|
||||||
|
Quaternion.Identity,
|
||||||
|
otherCell,
|
||||||
|
192f,
|
||||||
|
0f,
|
||||||
|
PhysicsShadowCommitAction.Replace,
|
||||||
|
[otherCell],
|
||||||
|
provenShapeless: false,
|
||||||
|
suspendOwner: false,
|
||||||
|
out var prepared));
|
||||||
|
Assert.True(registry.TryApplySetPosition(prepared!, out var receipt));
|
||||||
|
Assert.Empty(callbacks);
|
||||||
|
|
||||||
|
registry.DispatchSetPositionCommit(receipt);
|
||||||
|
registry.DispatchSetPositionCommit(receipt);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
[
|
||||||
|
"prefix:00000001:A9B40000",
|
||||||
|
"prefix:00000001:A9B50000",
|
||||||
|
$"owner:00000001:{receipt.OwnerVersion}",
|
||||||
|
], callbacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TwoOwnerReceiptsDispatchExactlyOnceInReverseOrder()
|
||||||
|
{
|
||||||
|
const uint otherCell = 0xA9B50001u;
|
||||||
|
var registry = RegisteredSingle();
|
||||||
|
registry.Register(
|
||||||
|
2u, 0x01000002u, new Vector3(13f, 12f, 50f),
|
||||||
|
Quaternion.Identity, 1f, 0f, 0f, Landblock,
|
||||||
|
seedCellId: Cell1, isStatic: false);
|
||||||
|
var owners = new List<uint>();
|
||||||
|
registry.OwnerMutated += (owner, _) => owners.Add(owner);
|
||||||
|
|
||||||
|
Assert.True(registry.TryPrepareSetPosition(
|
||||||
|
1u, new Vector3(204f, 12f, 50f), Quaternion.Identity,
|
||||||
|
otherCell, 192f, 0f, PhysicsShadowCommitAction.Replace,
|
||||||
|
[otherCell], provenShapeless: false, suspendOwner: false,
|
||||||
|
out var first));
|
||||||
|
Assert.True(registry.TryApplySetPosition(first!, out var firstReceipt));
|
||||||
|
Assert.True(registry.TryPrepareSetPosition(
|
||||||
|
2u, new Vector3(205f, 12f, 50f), Quaternion.Identity,
|
||||||
|
otherCell, 192f, 0f, PhysicsShadowCommitAction.Replace,
|
||||||
|
[otherCell], provenShapeless: false, suspendOwner: false,
|
||||||
|
out var second));
|
||||||
|
Assert.True(registry.TryApplySetPosition(second!, out var secondReceipt));
|
||||||
|
|
||||||
|
registry.DispatchSetPositionCommit(secondReceipt);
|
||||||
|
registry.DispatchSetPositionCommit(firstReceipt);
|
||||||
|
registry.DispatchSetPositionCommit(secondReceipt);
|
||||||
|
registry.DispatchSetPositionCommit(firstReceipt);
|
||||||
|
|
||||||
|
Assert.Equal([2u, 1u], owners);
|
||||||
|
Assert.Equal(0, registry.PendingSetPositionDispatchCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PrefixObserverMutationCannotRegressFinalOwnerVersion()
|
||||||
|
{
|
||||||
|
const uint otherCell = 0xA9B50001u;
|
||||||
|
var registry = RegisteredSingle();
|
||||||
|
var versions = new List<ulong>();
|
||||||
|
var prefixes = new List<uint>();
|
||||||
|
bool mutated = false;
|
||||||
|
registry.OwnerPrefixMembershipChanged += (owner, prefix) =>
|
||||||
|
{
|
||||||
|
prefixes.Add(prefix);
|
||||||
|
if (mutated)
|
||||||
|
return;
|
||||||
|
mutated = true;
|
||||||
|
registry.UpdatePhysicsState(
|
||||||
|
owner,
|
||||||
|
(uint)PhysicsStateFlags.Hidden);
|
||||||
|
};
|
||||||
|
registry.OwnerMutated += (_, version) => versions.Add(version);
|
||||||
|
Assert.True(registry.TryPrepareSetPosition(
|
||||||
|
1u, new Vector3(204f, 12f, 50f), Quaternion.Identity,
|
||||||
|
otherCell, 192f, 0f, PhysicsShadowCommitAction.Replace,
|
||||||
|
[otherCell], provenShapeless: false, suspendOwner: false,
|
||||||
|
out var prepared));
|
||||||
|
Assert.True(registry.TryApplySetPosition(prepared!, out var receipt));
|
||||||
|
|
||||||
|
registry.DispatchSetPositionCommit(receipt);
|
||||||
|
|
||||||
|
Assert.Single(versions);
|
||||||
|
Assert.Equal(registry.GetOwnerVersion(1u), versions[0]);
|
||||||
|
Assert.Equal([Landblock], prefixes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreparedShapelessRequiresExplicitDispositionAndCreatesNoRows()
|
||||||
|
{
|
||||||
|
var registry = new ShadowObjectRegistry();
|
||||||
|
Assert.False(registry.TryPrepareSetPosition(
|
||||||
|
77u,
|
||||||
|
Vector3.One,
|
||||||
|
Quaternion.Identity,
|
||||||
|
Cell1,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
PhysicsShadowCommitAction.Replace,
|
||||||
|
[Cell1],
|
||||||
|
provenShapeless: false,
|
||||||
|
suspendOwner: false,
|
||||||
|
out _));
|
||||||
|
Assert.True(registry.TryPrepareSetPosition(
|
||||||
|
77u,
|
||||||
|
Vector3.One,
|
||||||
|
Quaternion.Identity,
|
||||||
|
Cell1,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
PhysicsShadowCommitAction.Replace,
|
||||||
|
[Cell1],
|
||||||
|
provenShapeless: true,
|
||||||
|
suspendOwner: false,
|
||||||
|
out var prepared));
|
||||||
|
Assert.True(registry.TryApplySetPosition(prepared!, out var receipt));
|
||||||
|
Assert.False(receipt.Mutated);
|
||||||
|
Assert.Empty(registry.GetObjectsInCell(Cell1));
|
||||||
|
Assert.False(registry.TryApplySetPosition(prepared!, out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ClearInvalidatesPreparedUnappliedShapelessCommit()
|
||||||
|
{
|
||||||
|
var registry = new ShadowObjectRegistry();
|
||||||
|
Assert.True(registry.TryPrepareSetPosition(
|
||||||
|
77u,
|
||||||
|
Vector3.One,
|
||||||
|
Quaternion.Identity,
|
||||||
|
Cell1,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
PhysicsShadowCommitAction.Replace,
|
||||||
|
[Cell1],
|
||||||
|
provenShapeless: true,
|
||||||
|
suspendOwner: false,
|
||||||
|
out var prepared));
|
||||||
|
|
||||||
|
registry.Clear();
|
||||||
|
|
||||||
|
Assert.False(registry.TryApplySetPosition(prepared!, out _));
|
||||||
|
Assert.Equal(0, registry.PendingSetPositionDispatchCount);
|
||||||
|
Assert.Empty(registry.GetObjectsInCell(Cell1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreparedCommitRejectsStaleGlobalRevisionWithoutMutation()
|
||||||
|
{
|
||||||
|
var registry = RegisteredSingle();
|
||||||
|
Assert.True(registry.TryPrepareSetPosition(
|
||||||
|
1u, new Vector3(36f, 12f, 50f), Quaternion.Identity,
|
||||||
|
Cell9, 0f, 0f, PhysicsShadowCommitAction.Replace, [Cell9],
|
||||||
|
provenShapeless: false, suspendOwner: false, out var prepared));
|
||||||
|
registry.Register(
|
||||||
|
2u, 0x01000002u, new Vector3(13f, 12f, 50f),
|
||||||
|
Quaternion.Identity, 1f, 0f, 0f, Landblock,
|
||||||
|
seedCellId: Cell1, isStatic: false);
|
||||||
|
Vector3 before = registry.GetObjectsInCell(Cell1)
|
||||||
|
.Single(entry => entry.EntityId == 1u).Position;
|
||||||
|
|
||||||
|
Assert.False(registry.TryApplySetPosition(prepared!, out _));
|
||||||
|
|
||||||
|
Assert.Equal(before, registry.GetObjectsInCell(Cell1)
|
||||||
|
.Single(entry => entry.EntityId == 1u).Position);
|
||||||
|
Assert.Empty(registry.GetObjectsInCell(Cell9));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreparedCommitRejectsStaleOwnerVersionWithoutMutation()
|
||||||
|
{
|
||||||
|
var registry = RegisteredSingle();
|
||||||
|
Assert.True(registry.TryPrepareSetPosition(
|
||||||
|
1u, new Vector3(36f, 12f, 50f), Quaternion.Identity,
|
||||||
|
Cell9, 0f, 0f, PhysicsShadowCommitAction.Replace, [Cell9],
|
||||||
|
provenShapeless: false, suspendOwner: false, out var prepared));
|
||||||
|
registry.UpdatePhysicsState(1u, (uint)PhysicsStateFlags.Hidden);
|
||||||
|
ShadowEntry before = Assert.Single(registry.GetObjectsInCell(Cell1));
|
||||||
|
|
||||||
|
Assert.False(registry.TryApplySetPosition(prepared!, out _));
|
||||||
|
|
||||||
|
Assert.Equal(before, Assert.Single(registry.GetObjectsInCell(Cell1)));
|
||||||
|
Assert.Empty(registry.GetObjectsInCell(Cell9));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreparedApplyTailAllocatesZeroManagedBytes()
|
||||||
|
{
|
||||||
|
// Warm the generic owner-value and dictionary replacement paths.
|
||||||
|
var warm = RegisteredSingle();
|
||||||
|
Assert.True(warm.TryPrepareSetPosition(
|
||||||
|
1u, new Vector3(36f, 12f, 50f), Quaternion.Identity,
|
||||||
|
Cell9, 0f, 0f, PhysicsShadowCommitAction.Replace, [Cell9],
|
||||||
|
provenShapeless: false, suspendOwner: false, out var warmPrepared));
|
||||||
|
Assert.True(warm.TryApplySetPosition(warmPrepared!, out _));
|
||||||
|
|
||||||
|
var registry = RegisteredSingle();
|
||||||
|
Assert.True(registry.TryPrepareSetPosition(
|
||||||
|
1u, new Vector3(36f, 12f, 50f), Quaternion.Identity,
|
||||||
|
Cell9, 0f, 0f, PhysicsShadowCommitAction.Replace, [Cell9],
|
||||||
|
provenShapeless: false, suspendOwner: false, out var prepared));
|
||||||
|
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||||
|
|
||||||
|
bool applied = registry.TryApplySetPosition(prepared!, out _);
|
||||||
|
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||||
|
|
||||||
|
Assert.True(applied);
|
||||||
|
Assert.Equal(0, allocated);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeferredDispatchRejectsReceiptAfterInterveningOwnerMutation()
|
||||||
|
{
|
||||||
|
const uint otherCell = 0xA9B50001u;
|
||||||
|
var registry = RegisteredSingle();
|
||||||
|
var versions = new List<ulong>();
|
||||||
|
var prefixes = new List<uint>();
|
||||||
|
registry.OwnerMutated += (_, version) => versions.Add(version);
|
||||||
|
registry.OwnerPrefixMembershipChanged += (_, prefix) =>
|
||||||
|
prefixes.Add(prefix);
|
||||||
|
Assert.True(registry.TryPrepareSetPosition(
|
||||||
|
1u, new Vector3(204f, 12f, 50f), Quaternion.Identity,
|
||||||
|
otherCell, 192f, 0f, PhysicsShadowCommitAction.Replace,
|
||||||
|
[otherCell], provenShapeless: false, suspendOwner: false,
|
||||||
|
out var prepared));
|
||||||
|
Assert.True(registry.TryApplySetPosition(prepared!, out var receipt));
|
||||||
|
|
||||||
|
registry.UpdatePhysicsState(1u, (uint)PhysicsStateFlags.Hidden);
|
||||||
|
ulong current = registry.GetOwnerVersion(1u);
|
||||||
|
registry.DispatchSetPositionCommit(receipt);
|
||||||
|
|
||||||
|
Assert.Equal([current], versions);
|
||||||
|
Assert.Empty(prefixes);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void NoneRefreshesFrameWithoutChangingExactMembership()
|
public void NoneRefreshesFrameWithoutChangingExactMembership()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1156,6 +1156,13 @@ public class PlayerMovementControllerTests
|
||||||
candidate.CaptureMovementResult(mouseLookEvent: false));
|
candidate.CaptureMovementResult(mouseLookEvent: false));
|
||||||
Assert.Throws<InvalidOperationException>(() => _ = candidate.PhysicsBody);
|
Assert.Throws<InvalidOperationException>(() => _ = candidate.PhysicsBody);
|
||||||
|
|
||||||
|
candidate.BeginDormantSetPositionGroundPhase();
|
||||||
|
Assert.Throws<InvalidOperationException>(
|
||||||
|
candidate.CommitRuntimeActivationFrame);
|
||||||
|
Assert.Throws<InvalidOperationException>(
|
||||||
|
candidate.ActivateRuntimePublication);
|
||||||
|
candidate.EndDormantSetPositionGroundPhase();
|
||||||
|
|
||||||
candidate.ActivateRuntimePublication();
|
candidate.ActivateRuntimePublication();
|
||||||
Assert.Same(body, candidate.PhysicsBody);
|
Assert.Same(body, candidate.PhysicsBody);
|
||||||
_ = candidate.CaptureMovementResult(mouseLookEvent: false);
|
_ = candidate.CaptureMovementResult(mouseLookEvent: false);
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,30 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
|
||||||
Assert.True(token.IsValid);
|
Assert.True(token.IsValid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ActivationIdExhaustionMutatesNoCandidateOrCanonicalOwner()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture();
|
||||||
|
typeof(RuntimeLocalPlayerPhysicsPublicationState)
|
||||||
|
.GetField(
|
||||||
|
"_nextActivationId",
|
||||||
|
System.Reflection.BindingFlags.Instance
|
||||||
|
| System.Reflection.BindingFlags.NonPublic)!
|
||||||
|
.SetValue(fixture.Owner, ulong.MaxValue);
|
||||||
|
|
||||||
|
Assert.Throws<OverflowException>(() => fixture.Prepare());
|
||||||
|
|
||||||
|
Assert.Null(fixture.Record.PhysicsBody);
|
||||||
|
Assert.Null(fixture.Movement.Controller);
|
||||||
|
Assert.Equal(0, fixture.Owner.CaptureOwnership().CandidateCount);
|
||||||
|
Assert.Equal(0, fixture.Owner.CaptureOwnership().PendingActivationCount);
|
||||||
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
||||||
|
.IsExactPreparedPlacementCurrent(
|
||||||
|
fixture.Record,
|
||||||
|
fixture.Placement,
|
||||||
|
fixture.Command));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CommitOwnsExactDormantBodyAndControllerWithoutWorldEdges()
|
public void CommitOwnsExactDormantBodyAndControllerWithoutWorldEdges()
|
||||||
{
|
{
|
||||||
|
|
@ -115,6 +139,39 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
|
||||||
fixture.Owner.Commit(token));
|
fixture.Owner.Commit(token));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InitialPhysicsVectorsAndCoefficientsSeedCanonicalActivation()
|
||||||
|
{
|
||||||
|
Vector3 omega = new(1f, 2f, 3f);
|
||||||
|
using var fixture = new Fixture(
|
||||||
|
residentWorld: true,
|
||||||
|
initialVelocity: new Vector3(100f, 0f, 0f),
|
||||||
|
initialOmega: omega,
|
||||||
|
initialFriction: 0.25f,
|
||||||
|
initialElasticity: 0.08f);
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
|
||||||
|
fixture.Owner.Commit(fixture.Prepare(), out var token));
|
||||||
|
PhysicsBody body = Assert.IsType<PhysicsBody>(fixture.Record.PhysicsBody);
|
||||||
|
Assert.Equal(new Vector3(50f, 0f, 0f), body.Velocity);
|
||||||
|
Assert.Equal(omega, body.Omega);
|
||||||
|
Assert.Equal(0.25f, body.Friction);
|
||||||
|
Assert.Equal(0.08f, body.Elasticity);
|
||||||
|
Assert.False(body.InWorld);
|
||||||
|
Assert.False(body.TransientState.HasFlag(TransientStateFlags.Active));
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
|
||||||
|
fixture.Owner.EvaluateActivation(token, out var evaluation));
|
||||||
|
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
|
||||||
|
fixture.Owner.CommitActivation(evaluation, out _));
|
||||||
|
|
||||||
|
Assert.Equal(new Vector3(50f, 0f, 0f), body.Velocity);
|
||||||
|
Assert.Equal(omega, body.Omega);
|
||||||
|
Assert.Equal(0.25f, body.Friction);
|
||||||
|
Assert.Equal(0.08f, body.Elasticity);
|
||||||
|
Assert.True(body.InWorld);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DeferredActivationEvaluationLeavesExactOwnedGraphDormantAndRetryable()
|
public void DeferredActivationEvaluationLeavesExactOwnedGraphDormantAndRetryable()
|
||||||
{
|
{
|
||||||
|
|
@ -198,6 +255,752 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
|
||||||
AssertNotLive(fixture.Movement.Controller!);
|
AssertNotLive(fixture.Movement.Controller!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CommitActivationPublishesExactLiveRuntimeGraphOnce()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(residentWorld: true);
|
||||||
|
bool callbackSawExactGraph = false;
|
||||||
|
var placements = new PlacementObserver(delta =>
|
||||||
|
{
|
||||||
|
if (delta.Placement.Kind is not RuntimePlacementProjectionKind.Place)
|
||||||
|
return;
|
||||||
|
callbackSawExactGraph = fixture.Record.PhysicsBody is { InWorld: true }
|
||||||
|
&& fixture.Record.PhysicsHost is not null
|
||||||
|
&& fixture.Movement.Controller is { IsRuntimePublished: true }
|
||||||
|
&& fixture.Movement.Controller.Movement.MoveTo is not null
|
||||||
|
&& fixture.Record.ObjectClock.IsActive
|
||||||
|
&& fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record);
|
||||||
|
});
|
||||||
|
using IDisposable subscription = fixture.Lifetime.Events
|
||||||
|
.SubscribePlacement(placements);
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
|
||||||
|
fixture.Owner.Commit(
|
||||||
|
fixture.Prepare(),
|
||||||
|
out RuntimeLocalPlayerPhysicsActivationToken token));
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
|
||||||
|
fixture.Owner.EvaluateActivation(token, out var evaluation));
|
||||||
|
ulong clockEpoch = fixture.Record.ObjectClockEpoch;
|
||||||
|
|
||||||
|
RuntimeDormantSetPositionCommitStatus status = fixture.Owner
|
||||||
|
.CommitActivation(evaluation, out var projection);
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed, status);
|
||||||
|
Assert.True(projection.IsValid);
|
||||||
|
PhysicsBody body = Assert.IsType<PhysicsBody>(fixture.Record.PhysicsBody);
|
||||||
|
Assert.True(body.InWorld);
|
||||||
|
Assert.True(body.TransientState.HasFlag(TransientStateFlags.Active));
|
||||||
|
Assert.NotNull(fixture.Record.PhysicsHost);
|
||||||
|
Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
|
||||||
|
Assert.True(fixture.Record.ObjectClock.IsActive);
|
||||||
|
Assert.Equal(clockEpoch + 1UL, fixture.Record.ObjectClockEpoch);
|
||||||
|
Assert.True(fixture.Movement.Controller!.IsRuntimePublished);
|
||||||
|
RuntimePlacementDelta place = Assert.Single(placements.Deltas);
|
||||||
|
Assert.Equal(RuntimePlacementProjectionKind.Place,
|
||||||
|
place.Placement.Kind);
|
||||||
|
Assert.Equal(projection.Sequence,
|
||||||
|
place.Placement.Token.Sequence);
|
||||||
|
Assert.True(callbackSawExactGraph);
|
||||||
|
Assert.Equal(0, fixture.Owner.CaptureOwnership()
|
||||||
|
.PendingActivationCount);
|
||||||
|
Assert.Equal(RuntimeDormantSetPositionCommitStatus.RejectedAuthority,
|
||||||
|
fixture.Owner.CommitActivation(evaluation, out _));
|
||||||
|
Assert.Single(placements.Deltas);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeferredCommitWaitsThenRearmsSameLeaseAfterExactGenerationWake()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture();
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
|
||||||
|
fixture.Owner.Commit(
|
||||||
|
fixture.Prepare(),
|
||||||
|
out RuntimeLocalPlayerPhysicsActivationToken token));
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.DeferredCell,
|
||||||
|
fixture.Owner.EvaluateActivation(token, out var deferred));
|
||||||
|
Assert.Equal(RuntimeDormantSetPositionCommitStatus.DeferredCell,
|
||||||
|
fixture.Owner.CommitActivation(deferred, out var noProjection));
|
||||||
|
Assert.False(noProjection.IsValid);
|
||||||
|
Assert.False(fixture.Record.PhysicsBody!.InWorld);
|
||||||
|
Assert.Equal(1, fixture.Owner.CaptureOwnership()
|
||||||
|
.PendingActivationCount);
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.DeferredCell,
|
||||||
|
fixture.Owner.EvaluateActivation(token, out var waiting));
|
||||||
|
Assert.False(waiting.IsValid);
|
||||||
|
Assert.Equal(1, fixture.Owner.CaptureOwnership()
|
||||||
|
.PendingActivationCount);
|
||||||
|
|
||||||
|
const ulong generation = 1UL;
|
||||||
|
fixture.Lifetime.Physics.SetPosition.BeginCollisionGeneration(
|
||||||
|
Cell & 0xFFFF0000u,
|
||||||
|
generation);
|
||||||
|
fixture.Lifetime.Physics.Engine.AddLandblock(
|
||||||
|
Cell & 0xFFFF0000u,
|
||||||
|
new TerrainSurface(new byte[81], new float[256]),
|
||||||
|
Array.Empty<CellSurface>(),
|
||||||
|
Array.Empty<PortalPlane>(),
|
||||||
|
worldOffsetX: 0f,
|
||||||
|
worldOffsetY: 0f);
|
||||||
|
fixture.Lifetime.Physics.SetPosition.CommitCollisionGeneration(
|
||||||
|
Cell & 0xFFFF0000u,
|
||||||
|
generation,
|
||||||
|
ready: true);
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
|
||||||
|
fixture.Owner.EvaluateActivation(token, out var ready));
|
||||||
|
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
|
||||||
|
fixture.Owner.CommitActivation(ready, out var projection));
|
||||||
|
Assert.True(projection.IsValid);
|
||||||
|
Assert.True(fixture.Record.PhysicsBody!.InWorld);
|
||||||
|
Assert.Equal(0, fixture.Owner.CaptureOwnership()
|
||||||
|
.PendingActivationCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RejectedCommitAppliesResponseOnceAndRetainsRetryableLease()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(residentWorld: true);
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
|
||||||
|
fixture.Owner.Commit(
|
||||||
|
fixture.Prepare(),
|
||||||
|
out RuntimeLocalPlayerPhysicsActivationToken token));
|
||||||
|
PhysicsBody body = fixture.Record.PhysicsBody!;
|
||||||
|
Vector3 position = body.Position;
|
||||||
|
uint fullCell = fixture.Record.FullCellId;
|
||||||
|
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
|
||||||
|
static (_, phase, _, observed) => phase
|
||||||
|
is TransitionCellCollisionPhase.Objects
|
||||||
|
? TransitionState.Collided
|
||||||
|
: observed;
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.RejectedPlacement,
|
||||||
|
fixture.Owner.EvaluateActivation(token, out var rejected));
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeDormantSetPositionCommitStatus.RejectedPlacement,
|
||||||
|
fixture.Owner.CommitActivation(rejected, out var projection));
|
||||||
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
||||||
|
.TryCaptureDormantLocalActivationResult(
|
||||||
|
token.Placement,
|
||||||
|
out PhysicsSetPositionResult rejectedResult));
|
||||||
|
Assert.Equal(PhysicsSetPositionError.NoValidPosition,
|
||||||
|
rejectedResult.Error);
|
||||||
|
Assert.False(rejectedResult.CollisionHandlerResult);
|
||||||
|
Assert.False(projection.IsValid);
|
||||||
|
Assert.Equal(position, body.Position);
|
||||||
|
Assert.Equal(fullCell, fixture.Record.FullCellId);
|
||||||
|
Assert.False(body.InWorld);
|
||||||
|
Assert.Null(fixture.Record.PhysicsHost);
|
||||||
|
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
|
||||||
|
Assert.Equal(1, fixture.Owner.CaptureOwnership()
|
||||||
|
.PendingActivationCount);
|
||||||
|
Assert.Equal(RuntimeDormantSetPositionCommitStatus.RejectedAuthority,
|
||||||
|
fixture.Owner.CommitActivation(rejected, out _));
|
||||||
|
|
||||||
|
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook = null;
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
|
||||||
|
fixture.Owner.EvaluateActivation(token, out var retry));
|
||||||
|
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
|
||||||
|
fixture.Owner.CommitActivation(retry, out projection));
|
||||||
|
Assert.True(projection.IsValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UndefinedShadowDispositionIsRejectedBeforeCandidateConstruction()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(residentWorld: true);
|
||||||
|
var invalid = new RuntimeLocalPlayerPhysicsActivationPreparation(
|
||||||
|
0.48f,
|
||||||
|
1.835f,
|
||||||
|
(RuntimeLocalPlayerShadowDisposition)0x7F);
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority,
|
||||||
|
fixture.Owner.Prepare(
|
||||||
|
fixture.Record,
|
||||||
|
fixture.Placement,
|
||||||
|
fixture.Command,
|
||||||
|
PlayerMovementConstructionOptions.Fallback,
|
||||||
|
invalid,
|
||||||
|
out var token));
|
||||||
|
Assert.False(token.IsValid);
|
||||||
|
Assert.Null(fixture.Record.PhysicsBody);
|
||||||
|
Assert.Null(fixture.Movement.Controller);
|
||||||
|
Assert.Equal(0, fixture.Owner.CaptureOwnership().CandidateCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RejectedCommitMapsEligibleEnvironmentCallbackToCollided()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(residentWorld: true);
|
||||||
|
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
|
||||||
|
static (transition, _, _, _) =>
|
||||||
|
{
|
||||||
|
transition.CollisionInfo.CollidedWithEnvironment = true;
|
||||||
|
return TransitionState.Collided;
|
||||||
|
};
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
|
||||||
|
fixture.Owner.Commit(
|
||||||
|
fixture.Prepare(),
|
||||||
|
out RuntimeLocalPlayerPhysicsActivationToken token));
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.RejectedPlacement,
|
||||||
|
fixture.Owner.EvaluateActivation(token, out var rejected));
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeDormantSetPositionCommitStatus.RejectedPlacement,
|
||||||
|
fixture.Owner.CommitActivation(rejected, out _));
|
||||||
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
||||||
|
.TryCaptureDormantLocalActivationResult(
|
||||||
|
token.Placement,
|
||||||
|
out PhysicsSetPositionResult result));
|
||||||
|
Assert.Equal(PhysicsSetPositionError.Collided, result.Error);
|
||||||
|
Assert.True(result.CollisionHandlerResult);
|
||||||
|
Assert.Equal(1, fixture.Lifetime.Physics.CollisionReports
|
||||||
|
.CaptureOwnership().OwnerCount);
|
||||||
|
|
||||||
|
Assert.True(fixture.Owner.DiscardActivation(token));
|
||||||
|
|
||||||
|
Assert.Null(fixture.Record.PhysicsBody);
|
||||||
|
Assert.Null(fixture.Movement.Controller);
|
||||||
|
Assert.Equal(0, fixture.Lifetime.Physics.SetPosition
|
||||||
|
.CaptureOwnership().ActiveOperationCount);
|
||||||
|
RuntimeCollisionReportingOwnershipSnapshot ownership = fixture.Lifetime
|
||||||
|
.Physics.CollisionReports.CaptureOwnership();
|
||||||
|
Assert.Equal(0, ownership.OwnerCount);
|
||||||
|
Assert.Equal(0, ownership.TrackedObjectCount);
|
||||||
|
Assert.Equal(0, ownership.ReversePeerCount);
|
||||||
|
Assert.Equal(0, ownership.PendingSetPositionDispatchCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RegisteredAuthoredShadowActivatesAndMovesExactRows()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(
|
||||||
|
residentWorld: true,
|
||||||
|
shadowDisposition:
|
||||||
|
RuntimeLocalPlayerShadowDisposition.RegisteredAuthoredPayload);
|
||||||
|
uint localId = fixture.Record.Key!.Value.LocalEntityId;
|
||||||
|
fixture.Lifetime.Physics.Engine.ShadowObjects.Register(
|
||||||
|
localId,
|
||||||
|
SetupId,
|
||||||
|
fixture.Command.Physics.Position + Vector3.UnitX,
|
||||||
|
Quaternion.Identity,
|
||||||
|
0.48f,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
Cell & 0xFFFF0000u,
|
||||||
|
ShadowCollisionType.Sphere,
|
||||||
|
state: (uint)fixture.Record.FinalPhysicsState,
|
||||||
|
seedCellId: Cell,
|
||||||
|
isStatic: false);
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
|
||||||
|
fixture.Owner.Commit(
|
||||||
|
fixture.Prepare(),
|
||||||
|
out RuntimeLocalPlayerPhysicsActivationToken token));
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
|
||||||
|
fixture.Owner.EvaluateActivation(token, out var evaluation));
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
|
||||||
|
fixture.Owner.CommitActivation(evaluation, out _));
|
||||||
|
|
||||||
|
ShadowEntry row = Assert.Single(fixture.Lifetime.Physics.Engine
|
||||||
|
.ShadowObjects.AllEntriesForDebug(),
|
||||||
|
entry => entry.EntityId == localId);
|
||||||
|
Assert.Equal(fixture.Record.PhysicsBody!.Position, row.Position);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeferredAuthoredActivationSuspendsRowsAndExactWakeRestoresThem()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(
|
||||||
|
shadowDisposition:
|
||||||
|
RuntimeLocalPlayerShadowDisposition.RegisteredAuthoredPayload);
|
||||||
|
uint localId = fixture.Record.Key!.Value.LocalEntityId;
|
||||||
|
fixture.Lifetime.Physics.Engine.ShadowObjects.Register(
|
||||||
|
localId, SetupId, fixture.Command.Physics.Position,
|
||||||
|
Quaternion.Identity, 0.48f, 0f, 0f,
|
||||||
|
Cell & 0xFFFF0000u, ShadowCollisionType.Sphere,
|
||||||
|
state: (uint)fixture.Record.FinalPhysicsState,
|
||||||
|
seedCellId: Cell, isStatic: false);
|
||||||
|
Assert.Contains(fixture.Lifetime.Physics.Engine.ShadowObjects
|
||||||
|
.AllEntriesForDebug(), row => row.EntityId == localId);
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
|
||||||
|
fixture.Owner.Commit(fixture.Prepare(), out var token));
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.DeferredCell,
|
||||||
|
fixture.Owner.EvaluateActivation(token, out var deferred));
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeDormantSetPositionCommitStatus.DeferredCell,
|
||||||
|
fixture.Owner.CommitActivation(deferred, out _));
|
||||||
|
|
||||||
|
Assert.False(fixture.Record.PhysicsBody!.InWorld);
|
||||||
|
Assert.DoesNotContain(fixture.Lifetime.Physics.Engine.ShadowObjects
|
||||||
|
.AllEntriesForDebug(), row => row.EntityId == localId);
|
||||||
|
Assert.Equal(1, fixture.Lifetime.Physics.Engine.ShadowObjects
|
||||||
|
.SuspendedRegistrationCount);
|
||||||
|
Assert.Equal(0, fixture.Lifetime.Physics.Engine.ShadowObjects
|
||||||
|
.PendingSetPositionDispatchCount);
|
||||||
|
|
||||||
|
const ulong generation = 1UL;
|
||||||
|
fixture.Lifetime.Physics.SetPosition.BeginCollisionGeneration(
|
||||||
|
Cell & 0xFFFF0000u, generation);
|
||||||
|
fixture.Lifetime.Physics.Engine.AddLandblock(
|
||||||
|
Cell & 0xFFFF0000u,
|
||||||
|
new TerrainSurface(new byte[81], new float[256]),
|
||||||
|
Array.Empty<CellSurface>(), Array.Empty<PortalPlane>(), 0f, 0f);
|
||||||
|
fixture.Lifetime.Physics.SetPosition.CommitCollisionGeneration(
|
||||||
|
Cell & 0xFFFF0000u, generation, ready: true);
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
|
||||||
|
fixture.Owner.EvaluateActivation(token, out var ready));
|
||||||
|
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
|
||||||
|
fixture.Owner.CommitActivation(ready, out _));
|
||||||
|
Assert.Contains(fixture.Lifetime.Physics.Engine.ShadowObjects
|
||||||
|
.AllEntriesForDebug(), row => row.EntityId == localId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeferredDiscardTransfersSuspendedShadowToEntityForLaterReuse()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(
|
||||||
|
shadowDisposition:
|
||||||
|
RuntimeLocalPlayerShadowDisposition.RegisteredAuthoredPayload);
|
||||||
|
uint localId = fixture.Record.Key!.Value.LocalEntityId;
|
||||||
|
ShadowObjectRegistry shadows = fixture.Lifetime.Physics.Engine
|
||||||
|
.ShadowObjects;
|
||||||
|
shadows.Register(
|
||||||
|
localId, SetupId, fixture.Command.Physics.Position,
|
||||||
|
Quaternion.Identity, 0.48f, 0f, 0f,
|
||||||
|
Cell & 0xFFFF0000u, ShadowCollisionType.Sphere,
|
||||||
|
state: (uint)fixture.Record.FinalPhysicsState,
|
||||||
|
seedCellId: Cell, isStatic: false);
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
|
||||||
|
fixture.Owner.Commit(fixture.Prepare(), out var token));
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.DeferredCell,
|
||||||
|
fixture.Owner.EvaluateActivation(token, out var deferred));
|
||||||
|
Assert.Equal(RuntimeDormantSetPositionCommitStatus.DeferredCell,
|
||||||
|
fixture.Owner.CommitActivation(deferred, out _));
|
||||||
|
|
||||||
|
Assert.True(fixture.Owner.DiscardActivation(token));
|
||||||
|
|
||||||
|
Assert.Null(fixture.Record.PhysicsBody);
|
||||||
|
Assert.Null(fixture.Movement.Controller);
|
||||||
|
Assert.Equal(0, fixture.Lifetime.Physics.SetPosition
|
||||||
|
.CaptureOwnership().ActiveOperationCount);
|
||||||
|
Assert.Equal(1, shadows.SuspendedRegistrationCount);
|
||||||
|
Assert.Equal(0, shadows.PendingSetPositionDispatchCount);
|
||||||
|
Assert.DoesNotContain(shadows.AllEntriesForDebug(),
|
||||||
|
row => row.EntityId == localId);
|
||||||
|
|
||||||
|
const ulong generation = 1UL;
|
||||||
|
fixture.Lifetime.Physics.SetPosition.BeginCollisionGeneration(
|
||||||
|
Cell & 0xFFFF0000u, generation);
|
||||||
|
fixture.Lifetime.Physics.Engine.AddLandblock(
|
||||||
|
Cell & 0xFFFF0000u,
|
||||||
|
new TerrainSurface(new byte[81], new float[256]),
|
||||||
|
Array.Empty<CellSurface>(), Array.Empty<PortalPlane>(), 0f, 0f);
|
||||||
|
fixture.Lifetime.Physics.SetPosition.CommitCollisionGeneration(
|
||||||
|
Cell & 0xFFFF0000u, generation, ready: true);
|
||||||
|
fixture.RepreparePlacement();
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
|
||||||
|
fixture.Owner.Commit(fixture.Prepare(), out var retry));
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
|
||||||
|
fixture.Owner.EvaluateActivation(retry, out var ready));
|
||||||
|
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
|
||||||
|
fixture.Owner.CommitActivation(ready, out _));
|
||||||
|
|
||||||
|
Assert.Equal(0, shadows.SuspendedRegistrationCount);
|
||||||
|
Assert.Contains(shadows.AllEntriesForDebug(),
|
||||||
|
row => row.EntityId == localId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CollisionCallbackDeleteRetiresPrephaseWithoutShadowOrPlace()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(residentWorld: true);
|
||||||
|
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
|
||||||
|
static (transition, phase, _, observed) =>
|
||||||
|
{
|
||||||
|
if (phase is TransitionCellCollisionPhase.Environment)
|
||||||
|
transition.CollisionInfo.CollidedWithEnvironment = true;
|
||||||
|
return observed;
|
||||||
|
};
|
||||||
|
var placements = new PlacementObserver();
|
||||||
|
using IDisposable placementSubscription = fixture.Lifetime.Events
|
||||||
|
.SubscribePlacement(placements);
|
||||||
|
bool deleted = false;
|
||||||
|
var collisions = new PublicationCollisionObserver(ignoredReport =>
|
||||||
|
{
|
||||||
|
if (deleted)
|
||||||
|
return;
|
||||||
|
deleted = true;
|
||||||
|
Assert.True(fixture.Lifetime.TryAcceptDelete(
|
||||||
|
new DeleteObject.Parsed(
|
||||||
|
fixture.Record.ServerGuid,
|
||||||
|
fixture.Record.Incarnation),
|
||||||
|
isLocalPlayer: false,
|
||||||
|
removeRetainedObject: false,
|
||||||
|
out RuntimeEntityDeleteAcceptance acceptance));
|
||||||
|
fixture.Lifetime.CompleteAcceptedDelete(acceptance);
|
||||||
|
Assert.Null(fixture.Lifetime.RetireCanonicalOnly(fixture.Record));
|
||||||
|
});
|
||||||
|
using IDisposable collisionSubscription = fixture.Lifetime.Physics
|
||||||
|
.CollisionReports.Subscribe(collisions);
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
|
||||||
|
fixture.Owner.Commit(fixture.Prepare(), out var token));
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
|
||||||
|
fixture.Owner.EvaluateActivation(token, out var evaluation));
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeDormantSetPositionCommitStatus.RejectedAuthority,
|
||||||
|
fixture.Owner.CommitActivation(evaluation, out var projection));
|
||||||
|
|
||||||
|
Assert.True(deleted);
|
||||||
|
Assert.False(projection.IsValid);
|
||||||
|
Assert.DoesNotContain(placements.Deltas,
|
||||||
|
delta => delta.Placement.Kind is RuntimePlacementProjectionKind.Place);
|
||||||
|
Assert.Equal(0, fixture.Owner.CaptureOwnership().PendingActivationCount);
|
||||||
|
Assert.Null(fixture.Movement.Controller);
|
||||||
|
Assert.Equal(0, fixture.Lifetime.Physics.CollisionReports
|
||||||
|
.CaptureOwnership().PendingSetPositionDispatchCount);
|
||||||
|
Assert.Equal(0, fixture.Lifetime.Physics.Engine.ShadowObjects
|
||||||
|
.PendingSetPositionDispatchCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CollisionCallbackVectorRefreshesDormantBodyWithRetailClamp()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(residentWorld: true);
|
||||||
|
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
|
||||||
|
static (transition, phase, _, observed) =>
|
||||||
|
{
|
||||||
|
if (phase is TransitionCellCollisionPhase.Environment)
|
||||||
|
transition.CollisionInfo.CollidedWithEnvironment = true;
|
||||||
|
return observed;
|
||||||
|
};
|
||||||
|
bool updated = false;
|
||||||
|
Vector3 omega = new(1f, 2f, 3f);
|
||||||
|
var collisions = new PublicationCollisionObserver(ignoredReport =>
|
||||||
|
{
|
||||||
|
if (updated)
|
||||||
|
return;
|
||||||
|
updated = true;
|
||||||
|
Assert.True(fixture.Lifetime.TryApplyVector(
|
||||||
|
new VectorUpdate.Parsed(
|
||||||
|
fixture.Record.ServerGuid,
|
||||||
|
new Vector3(100f, 0f, 0f),
|
||||||
|
omega,
|
||||||
|
InstanceSequence: fixture.Record.Incarnation,
|
||||||
|
VectorSequence: 2),
|
||||||
|
acknowledgeProjection: null,
|
||||||
|
out _));
|
||||||
|
_ = ignoredReport;
|
||||||
|
});
|
||||||
|
using IDisposable subscription = fixture.Lifetime.Physics
|
||||||
|
.CollisionReports.Subscribe(collisions);
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
|
||||||
|
fixture.Owner.Commit(fixture.Prepare(), out var token));
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
|
||||||
|
fixture.Owner.EvaluateActivation(token, out var evaluation));
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
|
||||||
|
fixture.Owner.CommitActivation(evaluation, out _));
|
||||||
|
|
||||||
|
Assert.True(updated);
|
||||||
|
Assert.Equal(new Vector3(50f, 0f, 0f),
|
||||||
|
fixture.Record.PhysicsBody!.Velocity);
|
||||||
|
Assert.Equal(omega, fixture.Record.PhysicsBody.Omega);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CollisionCallbackAuthorityChangeRetiresInstalledTracking()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(residentWorld: true);
|
||||||
|
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
|
||||||
|
static (transition, phase, _, observed) =>
|
||||||
|
{
|
||||||
|
if (phase is TransitionCellCollisionPhase.Environment)
|
||||||
|
transition.CollisionInfo.CollidedWithEnvironment = true;
|
||||||
|
return observed;
|
||||||
|
};
|
||||||
|
bool invalidated = false;
|
||||||
|
var collisions = new PublicationCollisionObserver(_ =>
|
||||||
|
{
|
||||||
|
if (invalidated)
|
||||||
|
return;
|
||||||
|
invalidated = true;
|
||||||
|
fixture.Lifetime.Entities.AdvanceObjDescAuthority(fixture.Record);
|
||||||
|
});
|
||||||
|
using IDisposable subscription = fixture.Lifetime.Physics
|
||||||
|
.CollisionReports.Subscribe(collisions);
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
|
||||||
|
fixture.Owner.Commit(fixture.Prepare(), out var token));
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
|
||||||
|
fixture.Owner.EvaluateActivation(token, out var evaluation));
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeDormantSetPositionCommitStatus.RejectedAuthority,
|
||||||
|
fixture.Owner.CommitActivation(evaluation, out _));
|
||||||
|
|
||||||
|
RuntimeCollisionReportingOwnershipSnapshot ownership = fixture.Lifetime
|
||||||
|
.Physics.CollisionReports.CaptureOwnership();
|
||||||
|
Assert.True(invalidated);
|
||||||
|
Assert.Equal(0, ownership.OwnerCount);
|
||||||
|
Assert.Equal(0, ownership.TrackedObjectCount);
|
||||||
|
Assert.Equal(0, ownership.ReversePeerCount);
|
||||||
|
Assert.Equal(0, ownership.PendingSetPositionDispatchCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CollisionCallbackNewerPositionSuppressesReciprocalAndPreservesNewLease()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(residentWorld: true);
|
||||||
|
RuntimeEntityRecord target = fixture.Lifetime.RegisterEntity(
|
||||||
|
Spawn(0x70003002u, incarnation: 1)).Canonical!;
|
||||||
|
fixture.Lifetime.Entities.SetFullCell(
|
||||||
|
target, Cell, Cell & 0xFFFF0000u);
|
||||||
|
fixture.Lifetime.Entities.SetFinalPhysicsState(
|
||||||
|
target, PhysicsStateFlags.ReportCollisions);
|
||||||
|
var targetBody = new PhysicsBody
|
||||||
|
{
|
||||||
|
Position = new Vector3(1f, 2f, 3f),
|
||||||
|
Orientation = Quaternion.Identity,
|
||||||
|
State = PhysicsStateFlags.ReportCollisions,
|
||||||
|
InWorld = true,
|
||||||
|
};
|
||||||
|
targetBody.SnapToCell(Cell, targetBody.Position, targetBody.Position);
|
||||||
|
fixture.Lifetime.Entities.SetPhysicsBody(target, targetBody);
|
||||||
|
fixture.Lifetime.Physics.AcknowledgeSpatialProjection(
|
||||||
|
target, spatial: true);
|
||||||
|
fixture.Lifetime.Physics.Engine.ShadowObjects.Register(
|
||||||
|
target.Key!.Value.LocalEntityId,
|
||||||
|
SetupId,
|
||||||
|
targetBody.Position,
|
||||||
|
Quaternion.Identity,
|
||||||
|
0.48f,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
Cell & 0xFFFF0000u,
|
||||||
|
ShadowCollisionType.Sphere,
|
||||||
|
(uint)targetBody.State,
|
||||||
|
Cell,
|
||||||
|
isStatic: false);
|
||||||
|
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
|
||||||
|
(transition, phase, _, observed) =>
|
||||||
|
{
|
||||||
|
if (phase is TransitionCellCollisionPhase.Environment)
|
||||||
|
{
|
||||||
|
transition.CollisionInfo.CollideObjectGuids.Add(
|
||||||
|
target.Key.Value.LocalEntityId);
|
||||||
|
}
|
||||||
|
return observed;
|
||||||
|
};
|
||||||
|
bool updated = false;
|
||||||
|
RuntimeEntityPlacementToken newer = default;
|
||||||
|
var reports = new List<RuntimeCollisionReport>();
|
||||||
|
var collisions = new PublicationCollisionObserver(report =>
|
||||||
|
{
|
||||||
|
reports.Add(report);
|
||||||
|
if (updated || report.Kind is not RuntimeCollisionReportKind.ObjectCollision)
|
||||||
|
return;
|
||||||
|
updated = true;
|
||||||
|
Assert.True(fixture.Lifetime.TryApplyPosition(
|
||||||
|
new WorldSession.EntityPositionUpdate(
|
||||||
|
fixture.Record.ServerGuid,
|
||||||
|
new CreateObject.ServerPosition(
|
||||||
|
Cell, 4f, 5f, 3f, 1f, 0f, 0f, 0f),
|
||||||
|
Velocity: null,
|
||||||
|
PlacementId: null,
|
||||||
|
IsGrounded: true,
|
||||||
|
InstanceSequence: fixture.Record.Incarnation,
|
||||||
|
PositionSequence: 2,
|
||||||
|
TeleportSequence: 0,
|
||||||
|
ForcePositionSequence: 0),
|
||||||
|
isLocalPlayer: true,
|
||||||
|
forcePositionRotation: null,
|
||||||
|
currentLocalVelocity: null,
|
||||||
|
projectionRequiresTeleportHook: false,
|
||||||
|
acknowledgeProjection: null,
|
||||||
|
out _,
|
||||||
|
out _,
|
||||||
|
out _));
|
||||||
|
newer = fixture.Lifetime.Physics.SetPosition.BeginAuthoredPlacement(
|
||||||
|
fixture.Record,
|
||||||
|
fixture.Record.PositionAuthorityVersion,
|
||||||
|
RuntimeSetPositionOperationKind.LocalAuthoritative);
|
||||||
|
Assert.True(newer.IsValid);
|
||||||
|
});
|
||||||
|
using IDisposable subscription = fixture.Lifetime.Physics
|
||||||
|
.CollisionReports.Subscribe(collisions);
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
|
||||||
|
fixture.Owner.Commit(fixture.Prepare(), out var token));
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.RejectedPlacement,
|
||||||
|
fixture.Owner.EvaluateActivation(token, out var evaluation));
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeDormantSetPositionCommitStatus.RejectedAuthority,
|
||||||
|
fixture.Owner.CommitActivation(evaluation, out _));
|
||||||
|
|
||||||
|
Assert.True(updated);
|
||||||
|
Assert.DoesNotContain(reports, report =>
|
||||||
|
report.Kind is RuntimeCollisionReportKind.ObjectCollision
|
||||||
|
&& report.Recipient == target.Key);
|
||||||
|
Assert.Equal(1, fixture.Lifetime.Physics.SetPosition
|
||||||
|
.CaptureOwnership().ActiveOperationCount);
|
||||||
|
RuntimeCollisionReportingOwnershipSnapshot ownership = fixture.Lifetime
|
||||||
|
.Physics.CollisionReports.CaptureOwnership();
|
||||||
|
Assert.Equal(0, ownership.OwnerCount);
|
||||||
|
Assert.Equal(0, ownership.TrackedObjectCount);
|
||||||
|
Assert.Equal(0, ownership.ReversePeerCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HitGroundReapplyStaysDormantUntilFinalActivationTail()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(residentWorld: true);
|
||||||
|
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
|
||||||
|
static (transition, phase, _, observed) =>
|
||||||
|
{
|
||||||
|
if (phase is TransitionCellCollisionPhase.Environment)
|
||||||
|
{
|
||||||
|
transition.CollisionInfo.SetContactPlane(
|
||||||
|
new Plane(Vector3.UnitZ, -3f), Cell, isWater: false);
|
||||||
|
}
|
||||||
|
return observed;
|
||||||
|
};
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
|
||||||
|
fixture.Owner.Commit(fixture.Prepare(), out var token));
|
||||||
|
PlayerMovementController controller = fixture.Movement.Controller!;
|
||||||
|
bool observedDormant = false;
|
||||||
|
controller.BeginDormantSetPositionGroundPhase();
|
||||||
|
controller.Motion.InterpretedState.ForwardCommand =
|
||||||
|
MotionCommand.RunForward;
|
||||||
|
controller.Motion.InterpretedState.ForwardSpeed = 1f;
|
||||||
|
controller.Motion.DefaultSink = new CallbackMotionSink(() =>
|
||||||
|
{
|
||||||
|
observedDormant = true;
|
||||||
|
Assert.False(fixture.Record.PhysicsBody!.InWorld);
|
||||||
|
Assert.False(fixture.Record.PhysicsBody.TransientState
|
||||||
|
.HasFlag(TransientStateFlags.Active));
|
||||||
|
Assert.Null(fixture.Record.PhysicsHost);
|
||||||
|
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
|
||||||
|
Assert.False(fixture.Record.ObjectClock.IsActive);
|
||||||
|
});
|
||||||
|
controller.EndDormantSetPositionGroundPhase();
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
|
||||||
|
fixture.Owner.EvaluateActivation(token, out var evaluation));
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
|
||||||
|
fixture.Owner.CommitActivation(evaluation, out var projection));
|
||||||
|
|
||||||
|
Assert.True(observedDormant);
|
||||||
|
Assert.True(projection.IsValid);
|
||||||
|
Assert.True(fixture.Record.PhysicsBody!.InWorld);
|
||||||
|
Assert.NotNull(fixture.Record.PhysicsHost);
|
||||||
|
Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
|
||||||
|
Assert.True(fixture.Record.ObjectClock.IsActive);
|
||||||
|
Assert.True(controller.IsRuntimePublished);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HitGroundInvalidationRetiresInstalledBatchBeforeDispatch()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(residentWorld: true);
|
||||||
|
RuntimeEntityRecord target = fixture.Lifetime.RegisterEntity(
|
||||||
|
Spawn(0x70003003u, incarnation: 1)).Canonical!;
|
||||||
|
fixture.Lifetime.Entities.SetFullCell(
|
||||||
|
target, Cell, Cell & 0xFFFF0000u);
|
||||||
|
fixture.Lifetime.Entities.SetFinalPhysicsState(
|
||||||
|
target, PhysicsStateFlags.ReportCollisions);
|
||||||
|
var targetBody = new PhysicsBody
|
||||||
|
{
|
||||||
|
Position = new Vector3(100f, 100f, 3f),
|
||||||
|
Orientation = Quaternion.Identity,
|
||||||
|
State = PhysicsStateFlags.ReportCollisions,
|
||||||
|
InWorld = true,
|
||||||
|
TransientState = TransientStateFlags.Active,
|
||||||
|
};
|
||||||
|
targetBody.SnapToCell(Cell, targetBody.Position, targetBody.Position);
|
||||||
|
fixture.Lifetime.Entities.SetPhysicsBody(target, targetBody);
|
||||||
|
fixture.Lifetime.Physics.AcknowledgeSpatialProjection(target, true);
|
||||||
|
fixture.Lifetime.Physics.Engine.ShadowObjects.Register(
|
||||||
|
target.Key!.Value.LocalEntityId, SetupId, targetBody.Position,
|
||||||
|
Quaternion.Identity, 0.48f, 0f, 0f, Cell & 0xFFFF0000u,
|
||||||
|
ShadowCollisionType.Sphere, (uint)targetBody.State, Cell,
|
||||||
|
isStatic: false);
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
|
||||||
|
fixture.Owner.Commit(fixture.Prepare(), out var token));
|
||||||
|
PhysicsBody ownerBody = fixture.Record.PhysicsBody!;
|
||||||
|
ownerBody.InWorld = true;
|
||||||
|
ownerBody.TransientState |= TransientStateFlags.Active;
|
||||||
|
Assert.True(fixture.Lifetime.Physics.CollisionReports.HandleReports(
|
||||||
|
fixture.Record, ownerBody, 9d, false, false, false,
|
||||||
|
[target.Key.Value.LocalEntityId]));
|
||||||
|
ownerBody.InWorld = false;
|
||||||
|
ownerBody.TransientState &= ~TransientStateFlags.Active;
|
||||||
|
Assert.Equal(1, fixture.Lifetime.Physics.CollisionReports
|
||||||
|
.CaptureOwnership().TrackedObjectCount);
|
||||||
|
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
|
||||||
|
static (transition, phase, _, observed) =>
|
||||||
|
{
|
||||||
|
if (phase is TransitionCellCollisionPhase.Environment)
|
||||||
|
{
|
||||||
|
transition.CollisionInfo.SetContactPlane(
|
||||||
|
new Plane(Vector3.UnitZ, -3f), Cell, isWater: false);
|
||||||
|
}
|
||||||
|
return observed;
|
||||||
|
};
|
||||||
|
PlayerMovementController controller = fixture.Movement.Controller!;
|
||||||
|
bool invalidated = false;
|
||||||
|
controller.BeginDormantSetPositionGroundPhase();
|
||||||
|
controller.Motion.InterpretedState.ForwardCommand =
|
||||||
|
MotionCommand.RunForward;
|
||||||
|
controller.Motion.DefaultSink = new CallbackMotionSink(() =>
|
||||||
|
{
|
||||||
|
if (invalidated)
|
||||||
|
return;
|
||||||
|
invalidated = true;
|
||||||
|
fixture.Lifetime.Entities.AdvanceObjDescAuthority(fixture.Record);
|
||||||
|
});
|
||||||
|
controller.EndDormantSetPositionGroundPhase();
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
|
||||||
|
fixture.Owner.EvaluateActivation(token, out var evaluation));
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeDormantSetPositionCommitStatus.RejectedAuthority,
|
||||||
|
fixture.Owner.CommitActivation(evaluation, out _));
|
||||||
|
|
||||||
|
Assert.True(invalidated);
|
||||||
|
RuntimeCollisionReportingOwnershipSnapshot ownership = fixture.Lifetime
|
||||||
|
.Physics.CollisionReports.CaptureOwnership();
|
||||||
|
Assert.Equal(0, ownership.OwnerCount);
|
||||||
|
Assert.Equal(0, ownership.TrackedObjectCount);
|
||||||
|
Assert.Equal(0, ownership.ReversePeerCount);
|
||||||
|
Assert.Equal(0, ownership.PendingSetPositionDispatchCount);
|
||||||
|
Assert.Equal(0, fixture.Lifetime.Physics.Engine.ShadowObjects
|
||||||
|
.PendingSetPositionDispatchCount);
|
||||||
|
Assert.Equal(0, fixture.Lifetime.Physics.SetPosition
|
||||||
|
.CaptureOwnership().ActiveOperationCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HiddenNoDrawStateDoesNotCancelDormantActivation()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(residentWorld: true);
|
||||||
|
PhysicsStateFlags hidden = fixture.Record.FinalPhysicsState
|
||||||
|
| PhysicsStateFlags.Hidden
|
||||||
|
| PhysicsStateFlags.NoDraw;
|
||||||
|
fixture.Lifetime.Entities.SetFinalPhysicsState(fixture.Record, hidden);
|
||||||
|
fixture.RepreparePlacement();
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
|
||||||
|
fixture.Owner.Commit(
|
||||||
|
fixture.Prepare(),
|
||||||
|
out RuntimeLocalPlayerPhysicsActivationToken token));
|
||||||
|
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
|
||||||
|
fixture.Owner.EvaluateActivation(token, out var evaluation));
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
|
||||||
|
fixture.Owner.CommitActivation(evaluation, out var projection));
|
||||||
|
|
||||||
|
Assert.True(projection.IsValid);
|
||||||
|
Assert.True(fixture.Record.PhysicsBody!.InWorld);
|
||||||
|
Assert.True(fixture.Record.PhysicsBody.State.HasFlag(
|
||||||
|
PhysicsStateFlags.Hidden));
|
||||||
|
Assert.True(fixture.Record.PhysicsBody.State.HasFlag(
|
||||||
|
PhysicsStateFlags.NoDraw));
|
||||||
|
Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RejectedPlacementReceiptIsPureAndRetryableUnderSameLease()
|
public void RejectedPlacementReceiptIsPureAndRetryableUnderSameLease()
|
||||||
{
|
{
|
||||||
|
|
@ -813,6 +1616,7 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
|
||||||
fixture.Placement,
|
fixture.Placement,
|
||||||
fixture.Command,
|
fixture.Command,
|
||||||
PlayerMovementConstructionOptions.Fallback,
|
PlayerMovementConstructionOptions.Fallback,
|
||||||
|
fixture.ActivationPreparation,
|
||||||
out _));
|
out _));
|
||||||
Assert.Equal(1, fixture.Owner.CaptureOwnership()
|
Assert.Equal(1, fixture.Owner.CaptureOwnership()
|
||||||
.PendingActivationCount);
|
.PendingActivationCount);
|
||||||
|
|
@ -820,12 +1624,14 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
|
||||||
Assert.True(fixture.Owner.DiscardActivation(token));
|
Assert.True(fixture.Owner.DiscardActivation(token));
|
||||||
Assert.Equal(0, fixture.Owner.CaptureOwnership()
|
Assert.Equal(0, fixture.Owner.CaptureOwnership()
|
||||||
.PendingActivationCount);
|
.PendingActivationCount);
|
||||||
|
fixture.RepreparePlacement();
|
||||||
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Prepared,
|
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Prepared,
|
||||||
fixture.Owner.Prepare(
|
fixture.Owner.Prepare(
|
||||||
fixture.Record,
|
fixture.Record,
|
||||||
fixture.Placement,
|
fixture.Placement,
|
||||||
fixture.Command,
|
fixture.Command,
|
||||||
PlayerMovementConstructionOptions.Fallback,
|
PlayerMovementConstructionOptions.Fallback,
|
||||||
|
fixture.ActivationPreparation,
|
||||||
out _));
|
out _));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1077,6 +1883,7 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
|
||||||
fixture.Placement,
|
fixture.Placement,
|
||||||
fixture.Command,
|
fixture.Command,
|
||||||
PlayerMovementConstructionOptions.Fallback,
|
PlayerMovementConstructionOptions.Fallback,
|
||||||
|
fixture.ActivationPreparation,
|
||||||
out RuntimeLocalPlayerPhysicsPublicationToken token));
|
out RuntimeLocalPlayerPhysicsPublicationToken token));
|
||||||
Assert.False(token.IsValid);
|
Assert.False(token.IsValid);
|
||||||
Assert.Equal(0, fixture.Owner.CaptureOwnership().CandidateCount);
|
Assert.Equal(0, fixture.Owner.CaptureOwnership().CandidateCount);
|
||||||
|
|
@ -1095,6 +1902,7 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
|
||||||
fixture.Placement,
|
fixture.Placement,
|
||||||
fixture.Command,
|
fixture.Command,
|
||||||
PlayerMovementConstructionOptions.Fallback,
|
PlayerMovementConstructionOptions.Fallback,
|
||||||
|
fixture.ActivationPreparation,
|
||||||
out _));
|
out _));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1111,6 +1919,7 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
|
||||||
fixture.Placement,
|
fixture.Placement,
|
||||||
fixture.Command,
|
fixture.Command,
|
||||||
PlayerMovementConstructionOptions.Fallback,
|
PlayerMovementConstructionOptions.Fallback,
|
||||||
|
fixture.ActivationPreparation,
|
||||||
out _));
|
out _));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1360,7 +2169,14 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
|
||||||
|
|
||||||
internal Fixture(
|
internal Fixture(
|
||||||
bool preparePlacement = true,
|
bool preparePlacement = true,
|
||||||
bool residentWorld = false)
|
bool residentWorld = false,
|
||||||
|
RuntimeLocalPlayerShadowDisposition shadowDisposition =
|
||||||
|
RuntimeLocalPlayerShadowDisposition.ProvenShapeless,
|
||||||
|
float terrainHeight = 0f,
|
||||||
|
Vector3? initialVelocity = null,
|
||||||
|
Vector3? initialOmega = null,
|
||||||
|
float? initialFriction = null,
|
||||||
|
float? initialElasticity = null)
|
||||||
{
|
{
|
||||||
if (residentWorld)
|
if (residentWorld)
|
||||||
{
|
{
|
||||||
|
|
@ -1370,7 +2186,9 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
|
||||||
};
|
};
|
||||||
engine.AddLandblock(
|
engine.AddLandblock(
|
||||||
Cell & 0xFFFF0000u,
|
Cell & 0xFFFF0000u,
|
||||||
new TerrainSurface(new byte[81], new float[256]),
|
new TerrainSurface(
|
||||||
|
new byte[81],
|
||||||
|
Enumerable.Repeat(terrainHeight, 256).ToArray()),
|
||||||
Array.Empty<CellSurface>(),
|
Array.Empty<CellSurface>(),
|
||||||
Array.Empty<PortalPlane>(),
|
Array.Empty<PortalPlane>(),
|
||||||
worldOffsetX: 0f,
|
worldOffsetX: 0f,
|
||||||
|
|
@ -1390,8 +2208,18 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
|
||||||
Identity);
|
Identity);
|
||||||
Movement.AttachPhysicsPublication(Owner);
|
Movement.AttachPhysicsPublication(Owner);
|
||||||
Record = Lifetime.RegisterEntity(
|
Record = Lifetime.RegisterEntity(
|
||||||
Spawn(0x70003001u, incarnation: 1)).Canonical!;
|
Spawn(
|
||||||
|
0x70003001u,
|
||||||
|
incarnation: 1,
|
||||||
|
initialVelocity,
|
||||||
|
initialOmega,
|
||||||
|
initialFriction,
|
||||||
|
initialElasticity)).Canonical!;
|
||||||
Identity.ServerGuid = Record.ServerGuid;
|
Identity.ServerGuid = Record.ServerGuid;
|
||||||
|
ActivationPreparation = new(
|
||||||
|
Radius: 0.48f,
|
||||||
|
Height: 1.835f,
|
||||||
|
shadowDisposition);
|
||||||
if (preparePlacement)
|
if (preparePlacement)
|
||||||
RepreparePlacement();
|
RepreparePlacement();
|
||||||
}
|
}
|
||||||
|
|
@ -1403,6 +2231,8 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
|
||||||
internal RuntimeEntityRecord Record { get; }
|
internal RuntimeEntityRecord Record { get; }
|
||||||
internal RuntimeEntityPlacementToken Placement { get; private set; }
|
internal RuntimeEntityPlacementToken Placement { get; private set; }
|
||||||
internal RuntimeSetPositionCommand Command { get; private set; }
|
internal RuntimeSetPositionCommand Command { get; private set; }
|
||||||
|
internal RuntimeLocalPlayerPhysicsActivationPreparation
|
||||||
|
ActivationPreparation { get; }
|
||||||
|
|
||||||
internal void RepreparePlacement()
|
internal void RepreparePlacement()
|
||||||
{
|
{
|
||||||
|
|
@ -1440,6 +2270,7 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
|
||||||
Placement,
|
Placement,
|
||||||
Command,
|
Command,
|
||||||
PlayerMovementConstructionOptions.Fallback,
|
PlayerMovementConstructionOptions.Fallback,
|
||||||
|
ActivationPreparation,
|
||||||
out RuntimeLocalPlayerPhysicsPublicationToken token));
|
out RuntimeLocalPlayerPhysicsPublicationToken token));
|
||||||
Assert.Equal(Record.Key, token.Entity);
|
Assert.Equal(Record.Key, token.Entity);
|
||||||
Assert.Equal(Placement, token.Placement);
|
Assert.Equal(Placement, token.Placement);
|
||||||
|
|
@ -1464,7 +2295,50 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static WorldSession.EntitySpawn Spawn(uint guid, ushort incarnation)
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class PublicationCollisionObserver(
|
||||||
|
Action<RuntimeCollisionReport> onReport)
|
||||||
|
: IRuntimeCollisionReportObserver
|
||||||
|
{
|
||||||
|
public void OnCollisionReport(in RuntimeCollisionReport report) =>
|
||||||
|
onReport(report);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class CallbackMotionSink(Action onApply)
|
||||||
|
: IInterpretedMotionSink
|
||||||
|
{
|
||||||
|
public bool ApplyMotion(uint motion, float speed)
|
||||||
|
{
|
||||||
|
onApply();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool StopMotion(uint motion)
|
||||||
|
{
|
||||||
|
onApply();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorldSession.EntitySpawn Spawn(
|
||||||
|
uint guid,
|
||||||
|
ushort incarnation,
|
||||||
|
Vector3? initialVelocity = null,
|
||||||
|
Vector3? initialOmega = null,
|
||||||
|
float? initialFriction = null,
|
||||||
|
float? initialElasticity = null)
|
||||||
{
|
{
|
||||||
var position = new CreateObject.ServerPosition(
|
var position = new CreateObject.ServerPosition(
|
||||||
Cell,
|
Cell,
|
||||||
|
|
@ -1498,12 +2372,12 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
|
||||||
Parent: null,
|
Parent: null,
|
||||||
Children: null,
|
Children: null,
|
||||||
Scale: 1f,
|
Scale: 1f,
|
||||||
Friction: null,
|
Friction: initialFriction,
|
||||||
Elasticity: null,
|
Elasticity: initialElasticity,
|
||||||
Translucency: null,
|
Translucency: null,
|
||||||
Velocity: null,
|
Velocity: initialVelocity,
|
||||||
Acceleration: null,
|
Acceleration: null,
|
||||||
AngularVelocity: null,
|
AngularVelocity: initialOmega,
|
||||||
DefaultScriptType: null,
|
DefaultScriptType: null,
|
||||||
DefaultScriptIntensity: null,
|
DefaultScriptIntensity: null,
|
||||||
Timestamps: timestamps);
|
Timestamps: timestamps);
|
||||||
|
|
@ -1522,6 +2396,8 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
|
||||||
MotionTableId: 0x09000001u,
|
MotionTableId: 0x09000001u,
|
||||||
PhysicsState: physics.RawState,
|
PhysicsState: physics.RawState,
|
||||||
ObjectDescriptionFlags: 0x8u,
|
ObjectDescriptionFlags: 0x8u,
|
||||||
|
Friction: initialFriction,
|
||||||
|
Elasticity: initialElasticity,
|
||||||
InstanceSequence: incarnation,
|
InstanceSequence: incarnation,
|
||||||
MovementSequence: 1,
|
MovementSequence: 1,
|
||||||
ServerControlSequence: 1,
|
ServerControlSequence: 1,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,604 @@ public sealed class RuntimeCollisionReportingStateTests
|
||||||
private const uint Landblock = 0xA9B40000u;
|
private const uint Landblock = 0xA9B40000u;
|
||||||
private const uint Cell = Landblock | 0x0001u;
|
private const uint Cell = Landblock | 0x0001u;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreparedBatchDefersTrackingUntilOrderedDispatchAndDispatchesOnce()
|
||||||
|
{
|
||||||
|
using var lifetime = Lifetime();
|
||||||
|
RuntimeEntityRecord owner = Entity(
|
||||||
|
lifetime,
|
||||||
|
0x70001F01u,
|
||||||
|
1,
|
||||||
|
PhysicsStateFlags.ReportCollisions);
|
||||||
|
RuntimeEntityRecord target = Entity(
|
||||||
|
lifetime,
|
||||||
|
0x70001F02u,
|
||||||
|
1,
|
||||||
|
PhysicsStateFlags.ReportCollisions);
|
||||||
|
target.PhysicsBody!.TransientState |= TransientStateFlags.Contact;
|
||||||
|
RegisterDynamicShadow(lifetime, target);
|
||||||
|
var observer = new CollisionObserver();
|
||||||
|
using IDisposable subscription = lifetime.Physics.CollisionReports
|
||||||
|
.Subscribe(observer);
|
||||||
|
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
|
||||||
|
owner,
|
||||||
|
owner.PhysicsBody!,
|
||||||
|
physicsTime: 10d,
|
||||||
|
previousContact: false,
|
||||||
|
previousOnWalkable: false,
|
||||||
|
finalOnWalkable: false,
|
||||||
|
collidedWithEnvironment: false,
|
||||||
|
[target.Key!.Value.LocalEntityId],
|
||||||
|
out var prepared));
|
||||||
|
Assert.NotNull(prepared);
|
||||||
|
Assert.Empty(observer.Reports);
|
||||||
|
Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership()
|
||||||
|
.TrackedObjectCount);
|
||||||
|
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
|
||||||
|
prepared!,
|
||||||
|
out var receipt));
|
||||||
|
Assert.Empty(observer.Reports);
|
||||||
|
Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership()
|
||||||
|
.TrackedObjectCount);
|
||||||
|
Assert.Equal(1, lifetime.Physics.CaptureOwnership()
|
||||||
|
.PendingCollisionSetPositionDispatchCount);
|
||||||
|
|
||||||
|
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
|
||||||
|
Assert.Equal(2, observer.Reports.Count);
|
||||||
|
Assert.Equal(1, lifetime.Physics.CollisionReports.CaptureOwnership()
|
||||||
|
.TrackedObjectCount);
|
||||||
|
Assert.False(observer.Reports[0].RecipientWasInContact);
|
||||||
|
Assert.True(observer.Reports[0].OtherWasInContact);
|
||||||
|
Assert.True(observer.Reports[1].RecipientWasInContact);
|
||||||
|
Assert.False(observer.Reports[1].OtherWasInContact);
|
||||||
|
Assert.Equal(0, lifetime.Physics.CaptureOwnership()
|
||||||
|
.PendingCollisionSetPositionDispatchCount);
|
||||||
|
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
|
||||||
|
Assert.Equal(2, observer.Reports.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CombinedPhysicsLedgerIncludesPendingShadowSetPositionReceipt()
|
||||||
|
{
|
||||||
|
using var lifetime = Lifetime();
|
||||||
|
RuntimeEntityRecord owner = Entity(
|
||||||
|
lifetime, 0x70001F03u, 1, PhysicsStateFlags.None);
|
||||||
|
RegisterDynamicShadow(lifetime, owner);
|
||||||
|
ShadowObjectRegistry shadows = lifetime.Physics.Engine.ShadowObjects;
|
||||||
|
Assert.True(shadows.TryPrepareSetPosition(
|
||||||
|
owner.Key!.Value.LocalEntityId,
|
||||||
|
new Vector3(13f, 12f, 7f),
|
||||||
|
Quaternion.Identity,
|
||||||
|
Cell,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
PhysicsShadowCommitAction.Replace,
|
||||||
|
[Cell],
|
||||||
|
provenShapeless: false,
|
||||||
|
suspendOwner: false,
|
||||||
|
out var prepared));
|
||||||
|
Assert.True(shadows.TryApplySetPosition(prepared!, out var receipt));
|
||||||
|
|
||||||
|
Assert.Equal(1, lifetime.Physics.CaptureOwnership()
|
||||||
|
.PendingShadowSetPositionDispatchCount);
|
||||||
|
|
||||||
|
Assert.True(shadows.DiscardSetPositionCommit(receipt));
|
||||||
|
Assert.Equal(0, lifetime.Physics.CaptureOwnership()
|
||||||
|
.PendingShadowSetPositionDispatchCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TwoOwnerPreparedBatchesDispatchExactlyOnceInReverseOrder()
|
||||||
|
{
|
||||||
|
using var lifetime = Lifetime();
|
||||||
|
RuntimeEntityRecord first = Entity(
|
||||||
|
lifetime, 0x70001E01u, 1, PhysicsStateFlags.ReportCollisions);
|
||||||
|
RuntimeEntityRecord second = Entity(
|
||||||
|
lifetime, 0x70001E02u, 1, PhysicsStateFlags.ReportCollisions);
|
||||||
|
const uint staticId = 0x00F01E01u;
|
||||||
|
RegisterShadow(lifetime, staticId, PhysicsStateFlags.Static,
|
||||||
|
isStatic: true);
|
||||||
|
var observer = new CollisionObserver();
|
||||||
|
using IDisposable subscription = lifetime.Physics.CollisionReports
|
||||||
|
.Subscribe(observer);
|
||||||
|
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
|
||||||
|
first, first.PhysicsBody!, 10d, false, false, false, false,
|
||||||
|
[staticId], out var firstPrepared));
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
|
||||||
|
firstPrepared!, out var firstReceipt));
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
|
||||||
|
second, second.PhysicsBody!, 10d, false, false, false, false,
|
||||||
|
[staticId], out var secondPrepared));
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
|
||||||
|
secondPrepared!, out var secondReceipt));
|
||||||
|
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.DispatchSetPositionBatch(
|
||||||
|
secondReceipt));
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.DispatchSetPositionBatch(
|
||||||
|
firstReceipt));
|
||||||
|
Assert.False(lifetime.Physics.CollisionReports.DispatchSetPositionBatch(
|
||||||
|
secondReceipt));
|
||||||
|
Assert.False(lifetime.Physics.CollisionReports.DispatchSetPositionBatch(
|
||||||
|
firstReceipt));
|
||||||
|
|
||||||
|
Assert.Equal(2, observer.Reports.Count);
|
||||||
|
Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership()
|
||||||
|
.PendingSetPositionDispatchCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OldBatchRetirementCannotRemoveBatchInstalledByItsCallback()
|
||||||
|
{
|
||||||
|
using var lifetime = Lifetime();
|
||||||
|
RuntimeEntityRecord owner = Entity(
|
||||||
|
lifetime, 0x70001E03u, 1, PhysicsStateFlags.ReportCollisions);
|
||||||
|
const uint staticId = 0x00F01E03u;
|
||||||
|
RegisterShadow(lifetime, staticId, PhysicsStateFlags.Static,
|
||||||
|
isStatic: true);
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
|
||||||
|
owner, owner.PhysicsBody!, 10d, false, false, false, false,
|
||||||
|
[staticId], out var oldPrepared));
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
|
||||||
|
oldPrepared!, out var oldReceipt));
|
||||||
|
RuntimeCollisionReportingState.SetPositionCollisionBatchReceipt
|
||||||
|
newerReceipt = default;
|
||||||
|
bool installedNewer = false;
|
||||||
|
var observer = new CollisionObserver(ignoredReport =>
|
||||||
|
{
|
||||||
|
if (installedNewer)
|
||||||
|
return;
|
||||||
|
installedNewer = true;
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports
|
||||||
|
.TryPrepareSetPositionBatch(
|
||||||
|
owner, owner.PhysicsBody!, 11d, false, false, false, true,
|
||||||
|
ImmutableArray<uint>.Empty, out var newerPrepared));
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports
|
||||||
|
.TryInstallSetPositionBatch(newerPrepared!, out newerReceipt));
|
||||||
|
_ = lifetime.Physics.CollisionReports.DispatchSetPositionBatch(
|
||||||
|
newerReceipt);
|
||||||
|
_ = ignoredReport;
|
||||||
|
});
|
||||||
|
using IDisposable subscription = lifetime.Physics.CollisionReports
|
||||||
|
.Subscribe(observer);
|
||||||
|
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.DispatchSetPositionBatch(
|
||||||
|
oldReceipt));
|
||||||
|
Assert.True(installedNewer);
|
||||||
|
Assert.True(newerReceipt.IsValid);
|
||||||
|
Assert.Equal(1, lifetime.Physics.CollisionReports.CaptureOwnership()
|
||||||
|
.OwnerCount);
|
||||||
|
|
||||||
|
lifetime.Physics.CollisionReports.RetireSetPositionBatchOwner(
|
||||||
|
oldReceipt);
|
||||||
|
Assert.Equal(1, lifetime.Physics.CollisionReports.CaptureOwnership()
|
||||||
|
.OwnerCount);
|
||||||
|
|
||||||
|
lifetime.Physics.CollisionReports.RetireSetPositionBatchOwner(
|
||||||
|
newerReceipt);
|
||||||
|
Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership()
|
||||||
|
.OwnerCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreparedBatchTracksHiddenIgnoredPeerButSuppressesCallbacks()
|
||||||
|
{
|
||||||
|
using var lifetime = Lifetime();
|
||||||
|
RuntimeEntityRecord owner = Entity(
|
||||||
|
lifetime,
|
||||||
|
0x70001F11u,
|
||||||
|
1,
|
||||||
|
PhysicsStateFlags.ReportCollisions);
|
||||||
|
RuntimeEntityRecord target = Entity(
|
||||||
|
lifetime,
|
||||||
|
0x70001F12u,
|
||||||
|
1,
|
||||||
|
PhysicsStateFlags.ReportCollisions);
|
||||||
|
RegisterDynamicShadow(lifetime, target);
|
||||||
|
var observer = new CollisionObserver();
|
||||||
|
using IDisposable subscription = lifetime.Physics.CollisionReports
|
||||||
|
.Subscribe(observer);
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
|
||||||
|
owner,
|
||||||
|
owner.PhysicsBody!,
|
||||||
|
10d,
|
||||||
|
previousContact: false,
|
||||||
|
previousOnWalkable: false,
|
||||||
|
finalOnWalkable: false,
|
||||||
|
collidedWithEnvironment: false,
|
||||||
|
[target.Key!.Value.LocalEntityId],
|
||||||
|
out var prepared));
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
|
||||||
|
prepared!,
|
||||||
|
out var receipt));
|
||||||
|
|
||||||
|
PhysicsStateFlags hidden = PhysicsStateFlags.Hidden
|
||||||
|
| PhysicsStateFlags.IgnoreCollisions;
|
||||||
|
lifetime.Entities.SetFinalPhysicsState(target, hidden);
|
||||||
|
target.PhysicsBody!.State = hidden;
|
||||||
|
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
|
||||||
|
|
||||||
|
Assert.Empty(observer.Reports);
|
||||||
|
Assert.Equal(1, lifetime.Physics.CollisionReports.CaptureOwnership()
|
||||||
|
.TrackedObjectCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreparedBatchConvertsLiveReportAsEnvironmentAfterGroundEdge()
|
||||||
|
{
|
||||||
|
using var lifetime = Lifetime();
|
||||||
|
RuntimeEntityRecord owner = Entity(
|
||||||
|
lifetime,
|
||||||
|
0x70001F21u,
|
||||||
|
1,
|
||||||
|
PhysicsStateFlags.ReportCollisions);
|
||||||
|
RuntimeEntityRecord target = Entity(
|
||||||
|
lifetime,
|
||||||
|
0x70001F22u,
|
||||||
|
1,
|
||||||
|
PhysicsStateFlags.ReportCollisions);
|
||||||
|
RegisterDynamicShadow(lifetime, target);
|
||||||
|
var observer = new CollisionObserver();
|
||||||
|
using IDisposable subscription = lifetime.Physics.CollisionReports
|
||||||
|
.Subscribe(observer);
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
|
||||||
|
owner,
|
||||||
|
owner.PhysicsBody!,
|
||||||
|
10d,
|
||||||
|
previousContact: false,
|
||||||
|
previousOnWalkable: false,
|
||||||
|
finalOnWalkable: false,
|
||||||
|
collidedWithEnvironment: false,
|
||||||
|
[target.Key!.Value.LocalEntityId],
|
||||||
|
out var prepared));
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
|
||||||
|
prepared!,
|
||||||
|
out var receipt));
|
||||||
|
|
||||||
|
lifetime.Entities.SetFinalPhysicsState(
|
||||||
|
target,
|
||||||
|
PhysicsStateFlags.ReportAsEnvironment);
|
||||||
|
target.PhysicsBody!.State = PhysicsStateFlags.ReportAsEnvironment;
|
||||||
|
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
|
||||||
|
|
||||||
|
Assert.Single(observer.Reports);
|
||||||
|
Assert.Equal(RuntimeCollisionReportKind.EnvironmentCollision,
|
||||||
|
observer.Reports[0].Kind);
|
||||||
|
Assert.Equal(1, lifetime.Physics.CollisionReports.CaptureOwnership()
|
||||||
|
.TrackedObjectCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreparedEnvironmentBatchClearsCapturedMissileWithoutReport()
|
||||||
|
{
|
||||||
|
using var lifetime = Lifetime();
|
||||||
|
RuntimeEntityRecord owner = Entity(
|
||||||
|
lifetime,
|
||||||
|
0x70001F31u,
|
||||||
|
1,
|
||||||
|
PhysicsStateFlags.Missile | PhysicsStateFlags.AlignPath);
|
||||||
|
RegisterDynamicShadow(lifetime, owner);
|
||||||
|
const uint staticId = 0x00F01F31u;
|
||||||
|
RegisterShadow(
|
||||||
|
lifetime,
|
||||||
|
staticId,
|
||||||
|
PhysicsStateFlags.Static,
|
||||||
|
isStatic: true);
|
||||||
|
var observer = new CollisionObserver();
|
||||||
|
using IDisposable subscription = lifetime.Physics.CollisionReports
|
||||||
|
.Subscribe(observer);
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
|
||||||
|
owner,
|
||||||
|
owner.PhysicsBody!,
|
||||||
|
10d,
|
||||||
|
previousContact: false,
|
||||||
|
previousOnWalkable: false,
|
||||||
|
finalOnWalkable: false,
|
||||||
|
collidedWithEnvironment: false,
|
||||||
|
[staticId],
|
||||||
|
out var prepared));
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
|
||||||
|
prepared!,
|
||||||
|
out var receipt));
|
||||||
|
|
||||||
|
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
|
||||||
|
|
||||||
|
Assert.Empty(observer.Reports);
|
||||||
|
Assert.False(owner.FinalPhysicsState.HasFlag(PhysicsStateFlags.Missile));
|
||||||
|
Assert.False(owner.PhysicsBody!.State.HasFlag(PhysicsStateFlags.Missile));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreparedExpiredEndDegradesToMissingTargetAfterInstall()
|
||||||
|
{
|
||||||
|
using var lifetime = Lifetime();
|
||||||
|
RuntimeEntityRecord owner = Entity(
|
||||||
|
lifetime,
|
||||||
|
0x70001F41u,
|
||||||
|
1,
|
||||||
|
PhysicsStateFlags.ReportCollisions);
|
||||||
|
RuntimeEntityRecord target = Entity(
|
||||||
|
lifetime,
|
||||||
|
0x70001F42u,
|
||||||
|
1,
|
||||||
|
PhysicsStateFlags.ReportCollisions);
|
||||||
|
target.PhysicsBody!.TransientState |= TransientStateFlags.Contact;
|
||||||
|
RegisterDynamicShadow(lifetime, target);
|
||||||
|
var observer = new CollisionObserver();
|
||||||
|
using IDisposable subscription = lifetime.Physics.CollisionReports
|
||||||
|
.Subscribe(observer);
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
|
||||||
|
owner,
|
||||||
|
owner.PhysicsBody!,
|
||||||
|
10d,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
[target.Key!.Value.LocalEntityId],
|
||||||
|
out var start));
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
|
||||||
|
start!,
|
||||||
|
out var startReceipt));
|
||||||
|
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(startReceipt);
|
||||||
|
observer.Reports.Clear();
|
||||||
|
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
|
||||||
|
owner,
|
||||||
|
owner.PhysicsBody!,
|
||||||
|
11.0001d,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
ImmutableArray<uint>.Empty,
|
||||||
|
out var end));
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
|
||||||
|
end!,
|
||||||
|
out var endReceipt));
|
||||||
|
Assert.True(lifetime.TryAcceptDelete(
|
||||||
|
new DeleteObject.Parsed(target.ServerGuid, target.Incarnation),
|
||||||
|
isLocalPlayer: false,
|
||||||
|
removeRetainedObject: false,
|
||||||
|
out RuntimeEntityDeleteAcceptance acceptance));
|
||||||
|
lifetime.CompleteAcceptedDelete(acceptance);
|
||||||
|
Assert.Null(lifetime.RetireCanonicalOnly(target));
|
||||||
|
|
||||||
|
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(endReceipt);
|
||||||
|
|
||||||
|
RuntimeCollisionReport report = Assert.Single(observer.Reports);
|
||||||
|
Assert.Equal(RuntimeCollisionReportKind.ObjectCollisionEnd, report.Kind);
|
||||||
|
Assert.Equal(owner.Key, report.Recipient);
|
||||||
|
Assert.Equal(target.ServerGuid, report.OtherServerGuid);
|
||||||
|
Assert.False(report.OtherWasInContact);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreparedBatchPreservesPreLoopEnvironmentLatchUntilFinalSuffix()
|
||||||
|
{
|
||||||
|
using var lifetime = Lifetime();
|
||||||
|
RuntimeEntityRecord owner = Entity(
|
||||||
|
lifetime,
|
||||||
|
0x70001F51u,
|
||||||
|
1,
|
||||||
|
PhysicsStateFlags.ReportCollisions);
|
||||||
|
RuntimeEntityRecord target = Entity(
|
||||||
|
lifetime,
|
||||||
|
0x70001F52u,
|
||||||
|
1,
|
||||||
|
PhysicsStateFlags.ReportCollisions);
|
||||||
|
RegisterDynamicShadow(lifetime, target);
|
||||||
|
var observer = new CollisionObserver();
|
||||||
|
using IDisposable subscription = lifetime.Physics.CollisionReports
|
||||||
|
.Subscribe(observer);
|
||||||
|
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.HandleReports(
|
||||||
|
owner,
|
||||||
|
owner.PhysicsBody!,
|
||||||
|
9d,
|
||||||
|
previousContact: false,
|
||||||
|
previousOnWalkable: false,
|
||||||
|
collidedWithEnvironment: true,
|
||||||
|
ImmutableArray<uint>.Empty));
|
||||||
|
observer.Reports.Clear();
|
||||||
|
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
|
||||||
|
owner,
|
||||||
|
owner.PhysicsBody!,
|
||||||
|
10d,
|
||||||
|
previousContact: false,
|
||||||
|
previousOnWalkable: false,
|
||||||
|
finalOnWalkable: false,
|
||||||
|
collidedWithEnvironment: false,
|
||||||
|
[target.Key!.Value.LocalEntityId],
|
||||||
|
out var prepared));
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
|
||||||
|
prepared!,
|
||||||
|
out var receipt));
|
||||||
|
lifetime.Entities.SetFinalPhysicsState(
|
||||||
|
target,
|
||||||
|
PhysicsStateFlags.ReportAsEnvironment);
|
||||||
|
target.PhysicsBody!.State = PhysicsStateFlags.ReportAsEnvironment;
|
||||||
|
|
||||||
|
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
|
||||||
|
|
||||||
|
Assert.Empty(observer.Reports);
|
||||||
|
Assert.Equal(1, lifetime.Physics.CollisionReports.CaptureOwnership()
|
||||||
|
.TrackedObjectCount);
|
||||||
|
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
|
||||||
|
owner,
|
||||||
|
owner.PhysicsBody!,
|
||||||
|
11d,
|
||||||
|
previousContact: false,
|
||||||
|
previousOnWalkable: false,
|
||||||
|
finalOnWalkable: false,
|
||||||
|
collidedWithEnvironment: true,
|
||||||
|
ImmutableArray<uint>.Empty,
|
||||||
|
out var next));
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
|
||||||
|
next!,
|
||||||
|
out var nextReceipt));
|
||||||
|
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(nextReceipt);
|
||||||
|
|
||||||
|
RuntimeCollisionReport report = Assert.Single(observer.Reports);
|
||||||
|
Assert.Equal(RuntimeCollisionReportKind.EnvironmentCollision,
|
||||||
|
report.Kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreparedEnvironmentBatchStopsMissileAddedByCallback()
|
||||||
|
{
|
||||||
|
using var lifetime = Lifetime();
|
||||||
|
RuntimeEntityRecord owner = Entity(
|
||||||
|
lifetime,
|
||||||
|
0x70001F61u,
|
||||||
|
1,
|
||||||
|
PhysicsStateFlags.ReportCollisions);
|
||||||
|
RegisterDynamicShadow(lifetime, owner);
|
||||||
|
const uint staticId = 0x00F01F61u;
|
||||||
|
RegisterShadow(
|
||||||
|
lifetime,
|
||||||
|
staticId,
|
||||||
|
PhysicsStateFlags.Static,
|
||||||
|
isStatic: true);
|
||||||
|
var observer = new CollisionObserver(_ =>
|
||||||
|
{
|
||||||
|
PhysicsStateFlags callbackState = owner.PhysicsBody!.State
|
||||||
|
| PhysicsStateFlags.Missile
|
||||||
|
| PhysicsStateFlags.AlignPath;
|
||||||
|
lifetime.Entities.SetFinalPhysicsState(owner, callbackState);
|
||||||
|
owner.PhysicsBody.State = callbackState;
|
||||||
|
});
|
||||||
|
using IDisposable subscription = lifetime.Physics.CollisionReports
|
||||||
|
.Subscribe(observer);
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
|
||||||
|
owner,
|
||||||
|
owner.PhysicsBody!,
|
||||||
|
10d,
|
||||||
|
previousContact: false,
|
||||||
|
previousOnWalkable: false,
|
||||||
|
finalOnWalkable: false,
|
||||||
|
collidedWithEnvironment: false,
|
||||||
|
[staticId],
|
||||||
|
out var prepared));
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
|
||||||
|
prepared!,
|
||||||
|
out var receipt));
|
||||||
|
|
||||||
|
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
|
||||||
|
|
||||||
|
Assert.Single(observer.Reports);
|
||||||
|
Assert.False(owner.FinalPhysicsState.HasFlag(PhysicsStateFlags.Missile));
|
||||||
|
Assert.False(owner.PhysicsBody!.State.HasFlag(PhysicsStateFlags.Missile));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreparedExistingCollisionRefreshesLiveEtherealBeforeExpiry()
|
||||||
|
{
|
||||||
|
using var lifetime = Lifetime();
|
||||||
|
RuntimeEntityRecord owner = Entity(
|
||||||
|
lifetime,
|
||||||
|
0x70001F71u,
|
||||||
|
1,
|
||||||
|
PhysicsStateFlags.ReportCollisions);
|
||||||
|
RuntimeEntityRecord target = Entity(
|
||||||
|
lifetime,
|
||||||
|
0x70001F72u,
|
||||||
|
1,
|
||||||
|
PhysicsStateFlags.ReportCollisions);
|
||||||
|
RegisterDynamicShadow(lifetime, target);
|
||||||
|
var observer = new CollisionObserver();
|
||||||
|
using IDisposable subscription = lifetime.Physics.CollisionReports
|
||||||
|
.Subscribe(observer);
|
||||||
|
|
||||||
|
DispatchPrepared(
|
||||||
|
lifetime,
|
||||||
|
owner,
|
||||||
|
physicsTime: 10d,
|
||||||
|
[target.Key!.Value.LocalEntityId]);
|
||||||
|
observer.Reports.Clear();
|
||||||
|
PhysicsStateFlags ethereal = PhysicsStateFlags.ReportCollisions
|
||||||
|
| PhysicsStateFlags.Ethereal;
|
||||||
|
lifetime.Entities.SetFinalPhysicsState(target, ethereal);
|
||||||
|
target.PhysicsBody!.State = ethereal;
|
||||||
|
|
||||||
|
DispatchPrepared(
|
||||||
|
lifetime,
|
||||||
|
owner,
|
||||||
|
physicsTime: 10.5d,
|
||||||
|
[target.Key!.Value.LocalEntityId]);
|
||||||
|
Assert.Empty(observer.Reports);
|
||||||
|
DispatchPrepared(
|
||||||
|
lifetime,
|
||||||
|
owner,
|
||||||
|
physicsTime: 10.5001d,
|
||||||
|
ImmutableArray<uint>.Empty);
|
||||||
|
|
||||||
|
Assert.Equal(2, observer.Reports.Count);
|
||||||
|
Assert.All(observer.Reports, report => Assert.Equal(
|
||||||
|
RuntimeCollisionReportKind.ObjectCollisionEnd,
|
||||||
|
report.Kind));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreparedExistingCollisionBecomingStaticKeepsOldAgeForSamePassEnd()
|
||||||
|
{
|
||||||
|
using var lifetime = Lifetime();
|
||||||
|
RuntimeEntityRecord owner = Entity(
|
||||||
|
lifetime,
|
||||||
|
0x70001F81u,
|
||||||
|
1,
|
||||||
|
PhysicsStateFlags.ReportCollisions);
|
||||||
|
RuntimeEntityRecord target = Entity(
|
||||||
|
lifetime,
|
||||||
|
0x70001F82u,
|
||||||
|
1,
|
||||||
|
PhysicsStateFlags.ReportCollisions);
|
||||||
|
RegisterDynamicShadow(lifetime, target);
|
||||||
|
var observer = new CollisionObserver();
|
||||||
|
using IDisposable subscription = lifetime.Physics.CollisionReports
|
||||||
|
.Subscribe(observer);
|
||||||
|
DispatchPrepared(
|
||||||
|
lifetime,
|
||||||
|
owner,
|
||||||
|
physicsTime: 10d,
|
||||||
|
[target.Key!.Value.LocalEntityId]);
|
||||||
|
observer.Reports.Clear();
|
||||||
|
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
|
||||||
|
owner,
|
||||||
|
owner.PhysicsBody!,
|
||||||
|
11.0001d,
|
||||||
|
previousContact: false,
|
||||||
|
previousOnWalkable: false,
|
||||||
|
finalOnWalkable: false,
|
||||||
|
collidedWithEnvironment: false,
|
||||||
|
[target.Key!.Value.LocalEntityId],
|
||||||
|
out var prepared));
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
|
||||||
|
prepared!,
|
||||||
|
out var receipt));
|
||||||
|
PhysicsStateFlags becameStatic = PhysicsStateFlags.ReportCollisions
|
||||||
|
| PhysicsStateFlags.Static;
|
||||||
|
lifetime.Entities.SetFinalPhysicsState(target, becameStatic);
|
||||||
|
target.PhysicsBody!.State = becameStatic;
|
||||||
|
|
||||||
|
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
|
||||||
|
|
||||||
|
Assert.Equal(3, observer.Reports.Count);
|
||||||
|
Assert.Equal(RuntimeCollisionReportKind.EnvironmentCollision,
|
||||||
|
observer.Reports[0].Kind);
|
||||||
|
Assert.Equal(RuntimeCollisionReportKind.ObjectCollisionEnd,
|
||||||
|
observer.Reports[1].Kind);
|
||||||
|
Assert.Equal(RuntimeCollisionReportKind.ObjectCollisionEnd,
|
||||||
|
observer.Reports[2].Kind);
|
||||||
|
Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership()
|
||||||
|
.TrackedObjectCount);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ExactObjectReportsAreOrderedDeduplicatedAndExpireAtRetailThreshold()
|
public void ExactObjectReportsAreOrderedDeduplicatedAndExpireAtRetailThreshold()
|
||||||
{
|
{
|
||||||
|
|
@ -1656,6 +2254,28 @@ public sealed class RuntimeCollisionReportingStateTests
|
||||||
owner.PhysicsBody.OnWalkable,
|
owner.PhysicsBody.OnWalkable,
|
||||||
collision);
|
collision);
|
||||||
|
|
||||||
|
private static void DispatchPrepared(
|
||||||
|
RuntimeEntityObjectLifetime lifetime,
|
||||||
|
RuntimeEntityRecord owner,
|
||||||
|
double physicsTime,
|
||||||
|
ImmutableArray<uint> collidedObjectIds)
|
||||||
|
{
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports
|
||||||
|
.TryPrepareSetPositionBatch(
|
||||||
|
owner,
|
||||||
|
owner.PhysicsBody!,
|
||||||
|
physicsTime,
|
||||||
|
previousContact: owner.PhysicsBody!.InContact,
|
||||||
|
previousOnWalkable: owner.PhysicsBody.OnWalkable,
|
||||||
|
finalOnWalkable: owner.PhysicsBody.OnWalkable,
|
||||||
|
collidedWithEnvironment: false,
|
||||||
|
collidedObjectIds,
|
||||||
|
out var prepared));
|
||||||
|
Assert.True(lifetime.Physics.CollisionReports
|
||||||
|
.TryInstallSetPositionBatch(prepared!, out var receipt));
|
||||||
|
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
|
||||||
|
}
|
||||||
|
|
||||||
private static RuntimeSetPositionCommand PlacementCommand(
|
private static RuntimeSetPositionCommand PlacementCommand(
|
||||||
RuntimeEntityRecord owner,
|
RuntimeEntityRecord owner,
|
||||||
Vector3 position)
|
Vector3 position)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue