feat(runtime): commit dormant SetPosition activation

This commit is contained in:
Erik 2026-08-01 14:25:02 +02:00
parent 99f867f053
commit 5785a07b3e
13 changed files with 4674 additions and 111 deletions

View file

@ -2,19 +2,20 @@
## 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
one local-player `PhysicsBody` and `PlayerMovementController`, then retains an
exact post-ownership activation lease which can evaluate retail SetPosition
without publishing it. No App or
Headless production route invokes this transaction, so graphical and no-window
game behavior is unchanged and AP-1/AD-1 remain open.
one local-player `PhysicsBody` and `PlayerMovementController`, retains an exact
post-ownership evaluation lease, and commits the canonical Runtime SetPosition
activation in retail order. No App or Headless production route invokes this
transaction yet, so graphical and no-window game behavior is unchanged and
AP-1/AD-1 remain open until their hosts cut over.
The checkpoints deliberately stop before canonical SetPosition activation.
They do not consume the prepared placement operation, enter the body into the
physics engine, publish FullCell/world/host/shadow/workset state, or project a
presentation entity. Those effects belong to the next transaction and must all
use the same Runtime-owned dormant body.
Checkpoint 6 consumes the prepared placement operation only after the exact
body/controller/identity/collision envelope is current. It publishes FullCell,
world residence, host, shadow, workset, object-clock, and ordered Place state
from that same Runtime-owned dormant body. There is no second body, mirrored
gameplay owner, rollback mutation, or presentation callback inside the
canonical tail.
## Ownership contract
@ -46,8 +47,9 @@ the same server GUID as the exact entity incarnation.
Unpublished candidates and ownership-committed dormant controllers reject live
movement operations: update, public SetPosition, blip, outbound-position
capture, movement/position send tracking, and shared-engine position commit.
Only the subsequent activation transaction may promote `RuntimeOwnedDormant`
to `RuntimePublished`; this checkpoint never invokes that transition. Once a
Only the checkpoint-6 activation transaction may promote `RuntimeOwnedDormant`
to `RuntimePublished`; preparation and evaluation never invoke that transition.
Once a
Runtime-owned dormant or published controller is replaced, reset, or disposed,
its terminal retirement state rejects the same operations plus
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
worksets, shadows, collision-report owners, host, operation stage, or Place
projection. A missing cell therefore leaves the exact body dormant and the
authored operation retryable. A valid result likewise remains only an
immutable evaluation receipt; this checkpoint has no activation/commit API.
authored operation retryable. During evaluation, a valid result likewise
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
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
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
- Candidate privacy and live-operation rejection.
@ -161,15 +229,22 @@ activation in the ownership convergence ledger.
acquisition/binding, and remote-placement ownership.
- Exact local-player identity, identity-switch, and disposed-identity rejection.
- Exact same-body ownership in entity record and dormant movement controller.
- Dormant rejection after ownership commit plus the isolated controller-level
`dormant -> activated -> live` lifecycle contract for the next checkpoint.
- Initial PhysicsDesc velocity, angular velocity, friction, and elasticity
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,
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,
CreateObject, remote/projectile/body/clock/controller ownership, and explicit
placement cancellation.
- Delete plus same-GUID reincarnation.
- 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
body-state snapshots and no canonical, collision-report, projection,
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.
- Body/controller epochs advance only on actual ownership changes.
The checkpoint-5 focused publication suite passes 70/70 and the focused Core
SetPosition suite passes 59/59. The complete Runtime project passes 666/666;
the complete Core project passes 4,230 tests / 1 skip; and the App Runtime
physics/movement ownership guard passes 6/6. The complete Release solution
builds with zero errors (three pre-existing App-test nullability warnings are
outside this checkpoint), and its invariant-culture test gate passes 10,419
tests / 4 intentional skips. The installed prepared-package gate
uses the exact local `acdream.pak`. Under the machine's Swedish current culture,
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.
The checkpoint-6 focused publication/collision suite passes 129/129, the
focused Core shadow transaction suite passes 16/16, and the complete Runtime
project passes 695/695 under invariant globalization. The Runtime Release build
passes with zero warnings and zero errors. Broader Core/App/solution and
connected gates remain for the parent integration checkpoint. Under the
machine's Swedish current culture, the three previously known formatting
assertions remain unrelated (`0,5` versus `0.5` and localized sky text), so the
canonical Runtime gate runs under invariant globalization.
## Next checkpoint
Add the canonical Runtime SetPosition activation transaction. Before its
callback-free tail, it must prepare a presentation-independent Runtime
`PhysicsHost`/`PositionManager`/`MoveToManager` graph, the existing authoritative
authored shadow payload (without rebuilding AP-22 shapes), and an exact staged
collision-report batch. Then it may commit the already-owned dormant body,
contact/water/walkable/response state, object clock, FullCell, exact shadow,
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.
Cut the graphical and no-window local-player hosts over to this Runtime-owned
activation transaction, then delete their duplicate SetPosition
activation/publication paths. The cutover must preserve the same exact body,
controller, shadow payload, deferred-cell lease, collision receipt ordering,
and graceful teardown proven here; no host may reconstruct or replay the
canonical transaction.

View file

@ -199,6 +199,20 @@ public sealed class PhysicsBody
/// point unchanged for an in-range local.
/// </summary>
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;
uint cell = cellId;
@ -206,7 +220,6 @@ public sealed class PhysicsBody
if ((cellId & 0xFFFFu) is >= 1u and <= 0x40u)
LandDefs.AdjustToOutside(ref cell, ref local);
CellPosition = new Position(cell, new CellFrame(local, Orientation));
InWorld = true; // retail: enter_world / set_cell assigns physics_obj->cell
}
/// <summary>

View file

@ -128,27 +128,11 @@ public static class PhysicsObjUpdate
{
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;
bool finalOnWalkable = CommitSetPositionContactPrefix(
body,
inContact,
onWalkable,
previousOnWalkable);
if (!previousOnWalkable && finalOnWalkable)
{
@ -162,10 +146,44 @@ public static class PhysicsObjUpdate
if (isCurrent?.Invoke() == false)
return false;
}
body.calc_acceleration();
CommitSetPositionPostGround(body);
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>
/// retail <c>handle_all_collisions</c> (0x00514780). Reflects or zeros the body's
/// <see cref="PhysicsBody.Velocity"/> (retail m_velocityVector) based on

View file

@ -79,6 +79,10 @@ public sealed class ShadowObjectRegistry
private readonly HashSet<uint> _prefixScratch = new();
private readonly List<uint> _removedPrefixScratch = new();
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, 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(
uint entityId,
RegistrationRecord registration,
@ -1896,7 +2447,9 @@ public sealed class ShadowObjectRegistry
|| _entityToCells.Count != 0
|| _entityReg.Count != 0
|| _suspendedEntities.Count != 0
|| _suspendedEntityCells.Count != 0;
|| _suspendedEntityCells.Count != 0
|| _nextPreparedSetPositionCommitId
!= _lastAppliedSetPositionCommitId;
if (mutated)
AdvanceMutationRevision();
_cells.Clear();
@ -1916,6 +2469,7 @@ public sealed class ShadowObjectRegistry
_ownerFreeSlots.Clear();
_prefixScratch.Clear();
_removedPrefixScratch.Clear();
_pendingSetPositionDispatches.Clear();
_fallback = null;
}

View file

@ -707,6 +707,57 @@ public sealed class PlayerMovementController
internal bool IsRuntimeOwnedDormant => _publicationLifecycle
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()
{
if (_publicationLifecycle
@ -738,7 +789,8 @@ public sealed class PlayerMovementController
{
if (_publicationLifecycle
is not PlayerMovementControllerPublicationLifecycle
.RuntimeOwnedDormant)
.RuntimeOwnedDormant
|| _dormantSetPositionGroundPhase)
{
throw new InvalidOperationException(
"Only a dormant Runtime-owned movement controller can be activated.");
@ -747,6 +799,30 @@ public sealed class PlayerMovementController
.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()
{
if (_publicationLifecycle
@ -775,7 +851,8 @@ public sealed class PlayerMovementController
is PlayerMovementControllerPublicationLifecycle.StandalonePublished
or PlayerMovementControllerPublicationLifecycle
.CandidatePreparing
or PlayerMovementControllerPublicationLifecycle.RuntimePublished)
or PlayerMovementControllerPublicationLifecycle.RuntimePublished
|| IsRuntimeOwnedDormant && _dormantSetPositionGroundPhase)
{
return;
}
@ -807,6 +884,9 @@ public sealed class PlayerMovementController
if ((_body.State & PhysicsStateFlags.Static) != 0)
return;
if (IsRuntimeOwnedDormant && _dormantSetPositionGroundPhase)
return;
_objectClock.Activate();
_body.TransientState |= TransientStateFlags.Active;
}

View file

@ -1,5 +1,6 @@
using System.Numerics;
using AcDream.Core.Physics;
using AcDream.Core.Physics.Motion;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Physics;
@ -23,6 +24,26 @@ internal enum RuntimeLocalPlayerPhysicsActivationStatus
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(
RuntimeEntityKey Entity,
RuntimeEntityPlacementToken Placement,
@ -111,6 +132,12 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
{ get; init; }
internal required PlayerMovementController Controller { 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
@ -122,8 +149,15 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
{ get; init; }
internal required PlayerMovementController Controller { 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
{ get; set; }
internal RuntimeDormantSetPositionCommitReceipt PendingFinalCommit
{ get; set; }
}
private readonly RuntimeEntityDirectory _entities;
@ -135,6 +169,7 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
private ulong _nextPublicationId;
private ulong _nextActivationId;
private ulong _nextEvaluationId;
private long _activationDispatchFailureCount;
private bool _disposed;
internal RuntimeLocalPlayerPhysicsPublicationState(
@ -154,14 +189,25 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
in RuntimeEntityPlacementToken placement,
in RuntimeSetPositionCommand command,
PlayerMovementConstructionOptions options,
in RuntimeLocalPlayerPhysicsActivationPreparation activationPreparation,
out RuntimeLocalPlayerPhysicsPublicationToken token)
{
ObjectDisposedException.ThrowIf(_disposed, this);
ArgumentNullException.ThrowIfNull(record);
token = default;
if (!CanPrepare(record, placement, command))
if (!activationPreparation.IsValid
|| !CanPrepare(record, placement, command))
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(
_physics.Engine,
options);
@ -177,6 +223,69 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
controller.SetBodyOrientation(command.Physics.Orientation);
controller.ApplyPhysicsState(record.FinalPhysicsState);
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
// SetPosition transaction is the sole authority which may enter the
// body into world simulation and activate its ordinary workset.
@ -194,16 +303,41 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
}
DiscardCurrent();
_nextPublicationId = publicationId;
_nextActivationId = activationId;
token = new RuntimeLocalPlayerPhysicsPublicationToken(
record.Key.Value,
placement,
checked(++_nextPublicationId),
publicationId,
_identity.ServerGuid,
_identity.Revision,
record.PhysicsOwnershipEpoch,
record.ObjectClockEpoch,
_movement.ControllerOwnershipEpoch,
_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
{
Token = token,
@ -211,10 +345,27 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
PlacementCommand = command,
Controller = controller,
Body = body,
PhysicsHost = physicsHost,
Movement = movement,
Motion = motion,
ActivationPreparation = preparedActivation,
PreparedActivation = activationEnvelope,
};
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(
in RuntimeLocalPlayerPhysicsPublicationToken token) =>
Commit(token, out _);
@ -245,24 +396,8 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
candidate.Record.ObjectClock);
candidate.Record.SetPhysicsBody(candidate.Body);
_movement.CommitRuntimeOwnedController(candidate.Controller);
activationToken = new RuntimeLocalPlayerPhysicsActivationToken(
candidate.Token.Entity,
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,
};
activationToken = candidate.PreparedActivation.Token;
_activation = candidate.PreparedActivation;
_candidate = null;
return RuntimeLocalPlayerPhysicsPublicationStatus.Committed;
}
@ -291,6 +426,16 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
activation.PlacementCommand,
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)
&& !IsActivationCurrent(activation))
{
@ -329,6 +474,304 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
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(
in RuntimeLocalPlayerPhysicsActivationToken token)
{
@ -389,6 +832,9 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
_activation is null ? 0 : 1,
_nextPublicationId);
internal long ActivationDispatchFailureCount =>
_activationDispatchFailureCount;
internal void ResetSession()
{
ObjectDisposedException.ThrowIf(_disposed, this);
@ -501,15 +947,67 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
&& !activation.Record.ProjectileBindingInProgress
&& !activation.Record.RequiresRemotePlacementRuntime
&& !activation.Record.DeleteAcceptedForTeardown
&& _physics.SetPosition.IsExactPreparedPlacementCurrent(
&& _physics.SetPosition.IsDormantLocalActivationLeaseCurrent(
activation.Record,
activation.Body,
activation.Token.Placement,
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()
{
Activation? activation = _activation;
_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
&& _entities.IsCurrent(activation.Record)
&& ReferenceEquals(

View file

@ -11,6 +11,17 @@ internal enum RuntimeCollisionReportKind
EnvironmentCollision,
}
internal enum SetPositionCollisionBatchDispatchStatus : byte
{
RejectedReceipt,
Displaced,
Completed,
}
internal readonly record struct SetPositionCollisionBatchDispatchResult(
SetPositionCollisionBatchDispatchStatus Status,
bool Reported);
/// <summary>
/// Immutable presentation-free projection of one retail weenie collision
/// callback. Runtime commits the callback before an observer can re-enter.
@ -38,6 +49,7 @@ internal readonly record struct RuntimeCollisionReportingOwnershipSnapshot(
int PendingReportCount,
int LeavingOwnerCount,
int AdmissionBlockedOwnerCount,
int PendingSetPositionDispatchCount,
bool IsDispatching,
long DispatchFailureCount,
bool IsDisposed)
@ -51,6 +63,7 @@ internal readonly record struct RuntimeCollisionReportingOwnershipSnapshot(
&& PendingReportCount == 0
&& LeavingOwnerCount == 0
&& AdmissionBlockedOwnerCount == 0
&& PendingSetPositionDispatchCount == 0
&& !IsDispatching;
}
@ -78,6 +91,10 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
private ulong _nextSequence;
private ulong _dispatchEpoch = 1UL;
private long _dispatchFailureCount;
private ulong _mutationRevision;
private ulong _nextPreparedBatchId;
private ulong _lastInstalledBatchId;
private readonly HashSet<ulong> _pendingSetPositionDispatches = [];
private bool _dispatching;
private bool _disposed;
@ -102,6 +119,7 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
_pendingReports.Count,
_leaving.Count,
_admissionBlocked.Count,
_pendingSetPositionDispatches.Count,
_dispatching,
_dispatchFailureCount,
_disposed);
@ -125,6 +143,574 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
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>
/// Ports the reporting/tracking portion of retail
/// <c>CPhysicsObj::handle_all_collisions</c> (0x00514780). The return is
@ -148,6 +734,7 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
{
return false;
}
_mutationRevision = checked(_mutationRevision + 1UL);
bool reported = false;
if (collidedObjectIds.IsDefault)
@ -266,6 +853,7 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
ArgumentNullException.ThrowIfNull(record);
if (record.Key is not { } key)
return;
_mutationRevision = checked(_mutationRevision + 1UL);
if (!_admissionBlocked.Add(key))
return;
try
@ -288,6 +876,7 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
{
EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(records);
_mutationRevision = checked(_mutationRevision + 1UL);
var blocked = new List<(RuntimeEntityRecord Record, RuntimeEntityKey Key)>(
records.Count);
for (int index = 0; index < records.Count; index++)
@ -322,6 +911,7 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
ArgumentNullException.ThrowIfNull(record);
if (record.Key is not { } key)
return;
_mutationRevision = checked(_mutationRevision + 1UL);
// A session-clear batch blocks every owner before publishing the
// first force-end callback. A callback may synchronously accept the
// deletion of a later, already-blocked owner. That owner must still
@ -342,11 +932,13 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
internal void ResetSession()
{
EnsureNotDisposed();
_mutationRevision = checked(_mutationRevision + 1UL);
_owners.Clear();
_ownersByPeer.Clear();
_pendingReports.Clear();
_leaving.Clear();
_admissionBlocked.Clear();
_pendingSetPositionDispatches.Clear();
_dispatchEpoch = checked(_dispatchEpoch + 1UL);
}
@ -359,6 +951,7 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
_pendingReports.Clear();
_leaving.Clear();
_admissionBlocked.Clear();
_pendingSetPositionDispatches.Clear();
_observers = [];
_dispatchEpoch = checked(_dispatchEpoch + 1UL);
_disposed = true;
@ -372,7 +965,10 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
PhysicsBody targetBody,
RuntimeEntityKey targetKey,
PhysicsStateFlags targetState,
bool previousContact)
bool previousContact,
bool exactDormantOwner = false,
ulong expectedOwnerPositionAuthorityVersion = 0UL,
ulong setPositionBatchId = 0UL)
{
if ((targetState & PhysicsStateFlags.ReportAsEnvironment) != 0)
{
@ -380,7 +976,8 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
owner,
ownerBody,
ownerKey,
previousContact);
previousContact,
setPositionBatchId);
}
PhysicsStateFlags ownerState = ownerBody.State;
@ -416,7 +1013,13 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
// reentrant state update can therefore suppress this second report.
bool targetReported = IsCurrentParticipant(target, targetBody, targetKey)
&& (targetBody.State & PhysicsStateFlags.ReportCollisions) != 0
&& IsCurrentParticipant(owner, ownerBody, ownerKey)
&& (IsCurrentParticipant(owner, ownerBody, ownerKey)
|| exactDormantOwner
&& IsExactDormantParticipant(
owner,
ownerBody,
ownerKey,
expectedOwnerPositionAuthorityVersion))
&& (ownerBody.State & PhysicsStateFlags.IgnoreCollisions) == 0;
if (targetReported)
{
@ -433,13 +1036,31 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
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(
RuntimeEntityRecord owner,
PhysicsBody ownerBody,
RuntimeEntityKey ownerKey,
bool previousContact)
bool previousContact,
ulong setPositionBatchId = 0UL)
{
OwnerState state = GetOrCreateOwner(ownerKey);
OwnerState state = GetOrCreateOwner(
ownerKey, setPositionBatchId);
if (state.CollidingWithEnvironment)
return false;
@ -467,7 +1088,9 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
PhysicsBody? ownerBody,
RuntimeEntityKey ownerKey,
double physicsTime,
bool force)
bool force,
ulong setPositionBatchId = 0UL,
ulong expectedPositionAuthorityVersion = 0UL)
{
if (!_owners.TryGetValue(ownerKey, out OwnerState? state)
|| state.Records.Count == 0)
@ -524,7 +1147,15 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
|| reportEpoch != _dispatchEpoch
|| _entities.SessionLifetimeVersion != sourceSessionVersion
|| _entities.CurrentLifetimeMutation(owner.ServerGuid)
!= sourceLifetimeMutation)
!= sourceLifetimeMutation
|| setPositionBatchId != 0UL
&& (owner.PositionAuthorityVersion
!= expectedPositionAuthorityVersion
|| !_owners.TryGetValue(
ownerKey, out OwnerState? currentOwner)
|| !ReferenceEquals(currentOwner, state)
|| currentOwner.SetPositionBatchId
!= setPositionBatchId))
{
break;
}
@ -563,6 +1194,13 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
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(
RuntimeEntityRecord owner,
PhysicsBody? ownerBody,
@ -654,13 +1292,16 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
(uint)owner.FinalPhysicsState);
}
private OwnerState GetOrCreateOwner(RuntimeEntityKey key)
private OwnerState GetOrCreateOwner(
RuntimeEntityKey key,
ulong setPositionBatchId = 0UL)
{
if (!_owners.TryGetValue(key, out OwnerState? owner))
{
owner = new OwnerState();
_owners.Add(key, owner);
}
owner.SetPositionBatchId = setPositionBatchId;
return owner;
}
@ -874,15 +1515,16 @@ internal sealed class RuntimeCollisionReportingState : IDisposable
private void EnsureNotDisposed() =>
ObjectDisposedException.ThrowIf(_disposed, this);
private sealed class OwnerState
internal sealed class OwnerState
{
internal Dictionary<RuntimeEntityKey, CollisionRecord> Records { get; }
= new();
internal List<RuntimeEntityKey> Order { get; } = [];
internal bool CollidingWithEnvironment { get; set; }
internal ulong SetPositionBatchId { get; set; }
}
private readonly record struct CollisionRecord(
internal readonly record struct CollisionRecord(
double TouchedTime,
bool Ethereal,
uint ServerGuid);

View file

@ -32,6 +32,8 @@ public readonly record struct RuntimePhysicsOwnershipSnapshot(
int PendingCollisionReportCount,
int LeavingCollisionReportOwnerCount,
int CollisionReportAdmissionBlockedOwnerCount,
int PendingCollisionSetPositionDispatchCount,
int PendingShadowSetPositionDispatchCount,
bool IsCollisionReportDispatching,
int CollisionAdmissionCount,
int CollisionGenerationCount,
@ -66,6 +68,8 @@ public readonly record struct RuntimePhysicsOwnershipSnapshot(
&& PendingCollisionReportCount == 0
&& LeavingCollisionReportOwnerCount == 0
&& CollisionReportAdmissionBlockedOwnerCount == 0
&& PendingCollisionSetPositionDispatchCount == 0
&& PendingShadowSetPositionDispatchCount == 0
&& !IsCollisionReportDispatching
&& CollisionAdmissionCount == 0
&& CollisionGenerationCount == 0
@ -1162,6 +1166,8 @@ public sealed class RuntimePhysicsState : IDisposable
collisionReports.PendingReportCount,
collisionReports.LeavingOwnerCount,
collisionReports.AdmissionBlockedOwnerCount,
collisionReports.PendingSetPositionDispatchCount,
Engine.ShadowObjects.PendingSetPositionDispatchCount,
collisionReports.IsDispatching,
_collisionAdmissions.Count,
_collisionGenerations.Count,
@ -2529,6 +2535,18 @@ public sealed class RuntimePhysicsState : IDisposable
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)
{
uint landblockId = CanonicalLandblock(exactCellId);
@ -2671,6 +2689,32 @@ public sealed class RuntimePhysicsState : IDisposable
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(
RuntimeEntityRecord record,
ulong positionAuthorityVersion,

View file

@ -4,6 +4,7 @@ using AcDream.Core.Items;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
namespace AcDream.Runtime.Physics;
@ -29,6 +30,7 @@ internal enum RuntimeEntityPlacementStage
AwaitingPreparation,
AwaitingWithdrawalAcknowledgement,
AwaitingCell,
AwaitingFinalShadowPreparation,
AwaitingCommitAcknowledgement,
CancelledAwaitingAcknowledgement,
}
@ -109,6 +111,65 @@ internal readonly record struct RuntimeDormantSetPositionEvaluation(
&& 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(
ulong Sequence,
ulong Revision,
@ -258,6 +319,7 @@ internal sealed class RuntimeSetPositionState : IDisposable
internal List<RuntimeEntityKey>? LostFamilyKeys { get; set; }
internal bool InheritedLostDeadline { get; set; }
internal bool EnteringWorldFromCelllessResidence { get; set; }
internal bool DormantLocalActivation { get; set; }
internal RuntimeSetPositionCommand? PreparedCommandAwaitingWithdrawalAck
{
get;
@ -289,7 +351,7 @@ internal sealed class RuntimeSetPositionState : IDisposable
private readonly Dictionary<RuntimeEntityKey, Operation> _operations = [];
private readonly Dictionary<CellGenerationKey, List<RuntimeEntityKey>>
_deferredByCellGeneration = [];
private readonly SortedDictionary<ulong, RuntimePlacementProjectionSnapshot>
private SortedDictionary<ulong, RuntimePlacementProjectionSnapshot>
_pendingProjection = [];
private readonly List<CellGenerationKey> _deferredBucketOrder = [];
private readonly Dictionary<uint, List<RuntimeEntityKey>>
@ -558,8 +620,12 @@ internal sealed class RuntimeSetPositionState : IDisposable
if (!token.IsValid
|| !_operations.TryGetValue(token.Entity, out Operation? operation)
|| operation.Token != token
|| operation.Stage
is not RuntimeEntityPlacementStage.AwaitingPreparation
|| operation.Stage is not (
RuntimeEntityPlacementStage.AwaitingPreparation
or RuntimeEntityPlacementStage.AwaitingCell)
|| operation.Stage is RuntimeEntityPlacementStage.AwaitingCell
&& (!operation.DormantLocalActivation
|| !operation.WakeableLostCell)
|| !IsCurrent(operation)
|| !_moverPreparationAuthorities.TryGetValue(
token.Entity,
@ -641,6 +707,20 @@ internal sealed class RuntimeSetPositionState : IDisposable
ArgumentNullException.ThrowIfNull(record);
ArgumentNullException.ThrowIfNull(body);
evaluation = default;
if (!IsExactDormantLocalActivationCurrent(
record,
body,
token,
command,
out _)
&& !TryRearmDeferredDormantLocalActivation(
record,
body,
token,
command))
{
return false;
}
if (!IsExactDormantLocalActivationCurrent(
record,
body,
@ -716,6 +796,779 @@ internal sealed class RuntimeSetPositionState : IDisposable
&& _physics.IsCollisionEvaluationAuthorityCurrent(
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(
in RuntimeEntityPlacementToken token,
in RuntimeSetPositionCommand command) =>
@ -1559,6 +2412,12 @@ internal sealed class RuntimeSetPositionState : IDisposable
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)
|| !operation.WakeableLostCell
|| operation.RequiresPreparation
@ -2006,7 +2865,8 @@ internal sealed class RuntimeSetPositionState : IDisposable
in RuntimeEntityPlacementToken token,
in RuntimeSetPositionCommand command,
out Operation? operation,
bool allowCanonicalCommand = false)
bool allowCanonicalCommand = false,
bool allowDeferredLease = false)
{
operation = null;
if (!token.IsValid
@ -2021,6 +2881,14 @@ internal sealed class RuntimeSetPositionState : IDisposable
|| operation.Token != token
|| operation.Stage
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)
|| !IsCurrent(operation)
|| !ReferenceEquals(record.PhysicsBody, body)

View file

@ -10,6 +10,284 @@ public sealed class ShadowSetPositionCommitTests
private const uint Cell1 = Landblock | 0x0001u;
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]
public void NoneRefreshesFrameWithoutChangingExactMembership()
{

View file

@ -1156,6 +1156,13 @@ public class PlayerMovementControllerTests
candidate.CaptureMovementResult(mouseLookEvent: false));
Assert.Throws<InvalidOperationException>(() => _ = candidate.PhysicsBody);
candidate.BeginDormantSetPositionGroundPhase();
Assert.Throws<InvalidOperationException>(
candidate.CommitRuntimeActivationFrame);
Assert.Throws<InvalidOperationException>(
candidate.ActivateRuntimePublication);
candidate.EndDormantSetPositionGroundPhase();
candidate.ActivateRuntimePublication();
Assert.Same(body, candidate.PhysicsBody);
_ = candidate.CaptureMovementResult(mouseLookEvent: false);

View file

@ -45,6 +45,30 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
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]
public void CommitOwnsExactDormantBodyAndControllerWithoutWorldEdges()
{
@ -115,6 +139,39 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
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]
public void DeferredActivationEvaluationLeavesExactOwnedGraphDormantAndRetryable()
{
@ -198,6 +255,752 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
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]
public void RejectedPlacementReceiptIsPureAndRetryableUnderSameLease()
{
@ -813,6 +1616,7 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
fixture.Placement,
fixture.Command,
PlayerMovementConstructionOptions.Fallback,
fixture.ActivationPreparation,
out _));
Assert.Equal(1, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
@ -820,12 +1624,14 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
Assert.True(fixture.Owner.DiscardActivation(token));
Assert.Equal(0, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
fixture.RepreparePlacement();
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Prepared,
fixture.Owner.Prepare(
fixture.Record,
fixture.Placement,
fixture.Command,
PlayerMovementConstructionOptions.Fallback,
fixture.ActivationPreparation,
out _));
}
@ -1077,6 +1883,7 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
fixture.Placement,
fixture.Command,
PlayerMovementConstructionOptions.Fallback,
fixture.ActivationPreparation,
out RuntimeLocalPlayerPhysicsPublicationToken token));
Assert.False(token.IsValid);
Assert.Equal(0, fixture.Owner.CaptureOwnership().CandidateCount);
@ -1095,6 +1902,7 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
fixture.Placement,
fixture.Command,
PlayerMovementConstructionOptions.Fallback,
fixture.ActivationPreparation,
out _));
}
@ -1111,6 +1919,7 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
fixture.Placement,
fixture.Command,
PlayerMovementConstructionOptions.Fallback,
fixture.ActivationPreparation,
out _));
}
@ -1360,7 +2169,14 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
internal Fixture(
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)
{
@ -1370,7 +2186,9 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
};
engine.AddLandblock(
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<PortalPlane>(),
worldOffsetX: 0f,
@ -1390,8 +2208,18 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
Identity);
Movement.AttachPhysicsPublication(Owner);
Record = Lifetime.RegisterEntity(
Spawn(0x70003001u, incarnation: 1)).Canonical!;
Spawn(
0x70003001u,
incarnation: 1,
initialVelocity,
initialOmega,
initialFriction,
initialElasticity)).Canonical!;
Identity.ServerGuid = Record.ServerGuid;
ActivationPreparation = new(
Radius: 0.48f,
Height: 1.835f,
shadowDisposition);
if (preparePlacement)
RepreparePlacement();
}
@ -1403,6 +2231,8 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
internal RuntimeEntityRecord Record { get; }
internal RuntimeEntityPlacementToken Placement { get; private set; }
internal RuntimeSetPositionCommand Command { get; private set; }
internal RuntimeLocalPlayerPhysicsActivationPreparation
ActivationPreparation { get; }
internal void RepreparePlacement()
{
@ -1440,6 +2270,7 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
Placement,
Command,
PlayerMovementConstructionOptions.Fallback,
ActivationPreparation,
out RuntimeLocalPlayerPhysicsPublicationToken token));
Assert.Equal(Record.Key, token.Entity);
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(
Cell,
@ -1498,12 +2372,12 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
Parent: null,
Children: null,
Scale: 1f,
Friction: null,
Elasticity: null,
Friction: initialFriction,
Elasticity: initialElasticity,
Translucency: null,
Velocity: null,
Velocity: initialVelocity,
Acceleration: null,
AngularVelocity: null,
AngularVelocity: initialOmega,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
@ -1522,6 +2396,8 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
MotionTableId: 0x09000001u,
PhysicsState: physics.RawState,
ObjectDescriptionFlags: 0x8u,
Friction: initialFriction,
Elasticity: initialElasticity,
InstanceSequence: incarnation,
MovementSequence: 1,
ServerControlSequence: 1,

View file

@ -13,6 +13,604 @@ public sealed class RuntimeCollisionReportingStateTests
private const uint Landblock = 0xA9B40000u;
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]
public void ExactObjectReportsAreOrderedDeduplicatedAndExpireAtRetailThreshold()
{
@ -1656,6 +2254,28 @@ public sealed class RuntimeCollisionReportingStateTests
owner.PhysicsBody.OnWalkable,
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(
RuntimeEntityRecord owner,
Vector3 position)