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

@ -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)