feat(runtime): own SetPosition collision reports

This commit is contained in:
Erik 2026-08-01 00:15:11 +02:00
parent ec627c13a2
commit 237d1184d2
19 changed files with 3744 additions and 103 deletions

View file

@ -81,14 +81,53 @@ public static class PhysicsObjUpdate
Action? leaveGround = null,
Func<bool>? isCurrent = null,
Func<bool>? isVelocityCurrent = null)
{
if (!CommitSetPositionContactTransition(
body,
inContact,
onWalkable,
previousOnWalkable,
hitGround,
leaveGround,
isCurrent))
{
return false;
}
// Position, Vector, and Movement are independently timestamped but
// can all install m_velocityVector. If a later one arrived from a
// callback above, retain its vector and finish the non-overlapping
// contact/pose commit without applying this older collision response.
if (isVelocityCurrent?.Invoke() == false)
return isCurrent?.Invoke() ?? true;
HandleAllCollisions(
body,
collisionNormalValid,
collisionNormal,
previousContact,
previousOnWalkable,
body.OnWalkable);
return isCurrent?.Invoke() ?? true;
}
/// <summary>
/// Commits retail's Contact/OnWalkable and HitGround/LeaveGround prefix,
/// stopping immediately before <c>handle_all_collisions</c>. Runtime uses
/// this seam to run the canonical collision-table reports without adding
/// another per-placement delegate allocation.
/// </summary>
public static bool CommitSetPositionContactTransition(
PhysicsBody body,
bool inContact,
bool onWalkable,
bool previousOnWalkable,
Action? hitGround = null,
Action? leaveGround = null,
Func<bool>? isCurrent = null)
{
ArgumentNullException.ThrowIfNull(body);
// SetPositionInternal replaces Contact first but retains the source
// OnWalkable bit through its first calc_acceleration call. A deferred
// teleport may have parked the live body with both bits cleared, so
// restore the captured source bit explicitly before reproducing that
// ordering.
if (previousOnWalkable)
body.TransientState |= TransientStateFlags.OnWalkable;
else
@ -106,10 +145,6 @@ public static class PhysicsObjUpdate
else
body.TransientState &= ~TransientStateFlags.OnWalkable;
// AP-10 (Campaign P Slice P4, 2026-07-30): mirror WATER_CONTACT_TS
// alongside CONTACT_TS/ON_WALKABLE_TS, same as ApplySetPositionContact.
// Callers (e.g. RemoteTeleportPlacement) already set body.ContactPlaneIsWater
// before invoking this commit.
if (body.ContactPlaneIsWater)
body.TransientState |= TransientStateFlags.WaterContact;
else
@ -128,21 +163,6 @@ public static class PhysicsObjUpdate
return false;
}
body.calc_acceleration();
// Position, Vector, and Movement are independently timestamped but
// can all install m_velocityVector. If a later one arrived from a
// callback above, retain its vector and finish the non-overlapping
// contact/pose commit without applying this older collision response.
if (isVelocityCurrent?.Invoke() == false)
return isCurrent?.Invoke() ?? true;
HandleAllCollisions(
body,
collisionNormalValid,
collisionNormal,
previousContact,
previousOnWalkable,
finalOnWalkable);
return isCurrent?.Invoke() ?? true;
}
@ -179,14 +199,14 @@ public static class PhysicsObjUpdate
// is now owned by the SetPositionInternal-derived contact flags, not a Velocity.Z<=0
// gate). A grounded corridor wall-slide keeps its tangential velocity (should_reflect
// false), exactly as retail.
bool sledding = body.State.HasFlag(PhysicsStateFlags.Sledding);
bool sledding = (body.State & PhysicsStateFlags.Sledding) != 0;
bool shouldReflect = !(prevOnWalkable && nowOnWalkable && !sledding);
if (body.FramesStationaryFall <= 1)
{
if (shouldReflect && collisionNormalValid)
{
if (body.State.HasFlag(PhysicsStateFlags.Inelastic))
if ((body.State & PhysicsStateFlags.Inelastic) != 0)
{
body.Velocity = Vector3.Zero; // pc:282720-282722
}

View file

@ -1563,6 +1563,32 @@ public sealed class ShadowObjectRegistry
internal bool HasLogicalOwner(uint entityId) =>
_entityReg.ContainsKey(entityId);
/// <summary>
/// Returns the exact collision identity consumed by retail
/// <c>CPhysicsObj::track_object_collision</c>. Reporting must classify
/// an encountered object from the same retained shadow registration that
/// produced the collision; presence of an entity id alone is not enough
/// to infer either a static/environment collision or physics state.
/// </summary>
internal bool TryGetCollisionOwner(
uint entityId,
out uint physicsState,
out bool isStatic)
{
if (_entityReg.TryGetValue(
entityId,
out RegistrationRecord? registration))
{
physicsState = registration.State;
isStatic = registration.IsStatic;
return true;
}
physicsState = 0u;
isStatic = false;
return false;
}
public int PrefixOwnerSlotCapacityForDiagnostics(uint landblockId) =>
_prefixOwnerSlots.TryGetValue(
landblockId & 0xFFFF0000u,

View file

@ -323,6 +323,14 @@ public sealed class RuntimeEntityDirectory
record.FinalPhysicsState = state;
}
internal bool StopMissileAfterCollision(
RuntimeEntityRecord record,
bool requireCurrentMissile)
{
EnsureKnown(record);
return record.StopMissileAfterCollision(requireCurrentMissile);
}
public void SetHasPartArray(RuntimeEntityRecord record, bool value)
{
EnsureKnown(record);

View file

@ -450,6 +450,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
Physics.SetPosition.Forget(
canonical,
releasePreparedMover: true);
Physics.CollisionReports.Forget(canonical);
Physics.RemoveSpatialProjection(canonical);
Entities.SetRemoteMotion(canonical, null);
Entities.SetRemoteMotionBindingInProgress(canonical, false);
@ -540,6 +541,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
Entities.RefreshSnapshot(canonical, accepted);
Entities.AdvancePositionAuthority(canonical);
Physics.CollisionReports.LeaveWorld(canonical);
RuntimePlacementCancellationReceipt cancellation =
Physics.SetPosition.Forget(canonical);
Entities.SuspendObjectClock(canonical);
@ -631,6 +633,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
RuntimePlacementCancellationReceipt cancellation =
Physics.SetPosition.Forget(canonical);
Physics.CollisionReports.LeaveWorld(canonical);
Entities.SuspendObjectClock(canonical);
Entities.SetFullCell(canonical, 0u, 0u);
ulong spatialVersion = canonical.SpatialAuthorityVersion;
@ -730,6 +733,22 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
}
Entities.RefreshSnapshot(canonical, accepted);
RetailPhysicsStateTransition preview =
RetailPhysicsStateTransitions.Apply(
canonical.FinalPhysicsState,
(PhysicsStateFlags)update.PhysicsState);
ulong priorPhysicsMutation = canonical.PhysicsStateMutationVersion;
if (preview.HiddenTransition is RetailHiddenTransition.BecameHidden)
{
Physics.CollisionReports.LeaveWorld(canonical);
if (!Entities.IsCurrent(canonical)
|| canonical.PhysicsStateMutationVersion
!= priorPhysicsMutation)
{
transition = default;
return false;
}
}
transition = Entities.ApplyRawPhysicsState(
canonical,
update.PhysicsState);
@ -878,6 +897,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
if (!Entities.IsCurrent(canonical))
return false;
Physics.CollisionReports.LeaveWorld(canonical);
Entities.SuspendObjectClock(canonical);
Entities.SetFullCell(canonical, 0u, 0u);
ulong spatialVersion = canonical.SpatialAuthorityVersion;
@ -955,6 +975,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
&& active.Incarnation == delete.InstanceSequence
&& Entities.RemoveActive(active))
{
Physics.CollisionReports.Forget(active);
RuntimePlacementCancellationReceipt cancellation =
Physics.SetPosition.Forget(
active,
@ -1030,9 +1051,11 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
return Array.Empty<RuntimeEntityRecord>();
_sessionClearInProgress = true;
Physics.SetPosition.ResetSession();
Entities.BeginSessionClear();
RuntimeEntityRecord[] active = Entities.ActiveRecords.ToArray();
Physics.CollisionReports.LeaveWorldBatch(active);
Physics.SetPosition.ResetSession();
Physics.CollisionReports.ResetSession();
Entities.BeginSessionClear();
foreach (RuntimeEntityRecord canonical in active)
{
Physics.SetPosition.Forget(canonical);
@ -1166,6 +1189,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
Entities.RefreshSnapshot(canonical, accepted);
Entities.AdvancePositionAuthority(canonical);
Physics.CollisionReports.LeaveWorld(canonical);
RuntimePlacementCancellationReceipt cancellation =
Physics.SetPosition.Forget(canonical);
ulong positionVersion = canonical.PositionAuthorityVersion;

View file

@ -164,6 +164,29 @@ public sealed class RuntimeEntityRecord
PhysicsBody.State = FinalPhysicsState;
}
/// <summary>
/// Retail collision reporting clears Missile, AlignPath, and PathClipped
/// directly on the live CPhysicsObj. Keep the canonical record and its
/// borrowed body in the same mutation edge.
/// </summary>
internal bool StopMissileAfterCollision(bool requireCurrentMissile)
{
const PhysicsStateFlags stopped = PhysicsStateFlags.Missile
| PhysicsStateFlags.AlignPath
| PhysicsStateFlags.PathClipped;
if (requireCurrentMissile
&& (FinalPhysicsState & PhysicsStateFlags.Missile) == 0)
return false;
PhysicsStateFlags final = FinalPhysicsState & ~stopped;
if (final == FinalPhysicsState)
return false;
PhysicsStateMutationVersion++;
FinalPhysicsState = final;
if (PhysicsBody is not null)
PhysicsBody.State = FinalPhysicsState;
return true;
}
internal void RefreshDerivedState(bool refreshPosition = true)
{
if (refreshPosition && Snapshot.Position is { } position)

View file

@ -0,0 +1,918 @@
using System.Collections.Immutable;
using AcDream.Core.Physics;
using AcDream.Runtime.Entities;
namespace AcDream.Runtime.Physics;
internal enum RuntimeCollisionReportKind
{
ObjectCollision,
ObjectCollisionEnd,
EnvironmentCollision,
}
/// <summary>
/// Immutable presentation-free projection of one retail weenie collision
/// callback. Runtime commits the callback before an observer can re-enter.
/// </summary>
internal readonly record struct RuntimeCollisionReport(
ulong Sequence,
RuntimeCollisionReportKind Kind,
RuntimeEntityKey Recipient,
uint RecipientServerGuid,
RuntimeEntityKey? Other,
uint? OtherServerGuid,
bool RecipientWasInContact,
bool OtherWasInContact);
internal interface IRuntimeCollisionReportObserver
{
void OnCollisionReport(in RuntimeCollisionReport report);
}
internal readonly record struct RuntimeCollisionReportingOwnershipSnapshot(
int OwnerCount,
int TrackedObjectCount,
int ReversePeerCount,
int ObserverCount,
int PendingReportCount,
int LeavingOwnerCount,
int AdmissionBlockedOwnerCount,
bool IsDispatching,
long DispatchFailureCount,
bool IsDisposed)
{
internal bool IsConverged =>
IsDisposed
&& OwnerCount == 0
&& TrackedObjectCount == 0
&& ReversePeerCount == 0
&& ObserverCount == 0
&& PendingReportCount == 0
&& LeavingOwnerCount == 0
&& AdmissionBlockedOwnerCount == 0
&& !IsDispatching;
}
/// <summary>
/// Runtime owner for retail <c>CPhysicsObj::collision_table</c> and
/// <c>colliding_with_environment</c>.
/// </summary>
/// <remarks>
/// Records are keyed by exact <see cref="RuntimeEntityKey"/> and retain the
/// peer's server GUID. A deleted incarnation therefore cannot donate contact
/// state to a later GUID reuse, while retail's missing-object collision-end
/// callback can still name the departed server object.
/// </remarks>
internal sealed class RuntimeCollisionReportingState : IDisposable
{
private readonly RuntimeEntityDirectory _entities;
private readonly ShadowObjectRegistry _shadows;
private readonly Dictionary<RuntimeEntityKey, OwnerState> _owners = new();
private readonly Dictionary<RuntimeEntityKey, List<RuntimeEntityKey>>
_ownersByPeer = new();
private readonly Queue<PendingReport> _pendingReports = new();
private readonly HashSet<RuntimeEntityKey> _leaving = [];
private readonly HashSet<RuntimeEntityKey> _admissionBlocked = [];
private IRuntimeCollisionReportObserver[] _observers = [];
private ulong _nextSequence;
private ulong _dispatchEpoch = 1UL;
private long _dispatchFailureCount;
private bool _dispatching;
private bool _disposed;
internal RuntimeCollisionReportingState(
RuntimeEntityDirectory entities,
ShadowObjectRegistry shadows)
{
_entities = entities ?? throw new ArgumentNullException(nameof(entities));
_shadows = shadows ?? throw new ArgumentNullException(nameof(shadows));
}
internal RuntimeCollisionReportingOwnershipSnapshot CaptureOwnership()
{
int tracked = 0;
foreach ((_, OwnerState owner) in _owners)
tracked += owner.Records.Count;
return new RuntimeCollisionReportingOwnershipSnapshot(
_owners.Count,
tracked,
_ownersByPeer.Count,
_observers.Length,
_pendingReports.Count,
_leaving.Count,
_admissionBlocked.Count,
_dispatching,
_dispatchFailureCount,
_disposed);
}
internal IDisposable Subscribe(IRuntimeCollisionReportObserver observer)
{
EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(observer);
if (Array.IndexOf(_observers, observer) >= 0)
{
throw new InvalidOperationException(
"A collision-report observer cannot be subscribed twice.");
}
var replacement = new IRuntimeCollisionReportObserver[
_observers.Length + 1];
Array.Copy(_observers, replacement, _observers.Length);
replacement[^1] = observer;
_observers = replacement;
return new Subscription(this, observer);
}
/// <summary>
/// Ports the reporting/tracking portion of retail
/// <c>CPhysicsObj::handle_all_collisions</c> (0x00514780). The return is
/// true only when at least one weenie collision callback was produced by
/// this invocation; physical collision response is deliberately separate.
/// </summary>
internal bool HandleReports(
RuntimeEntityRecord owner,
PhysicsBody ownerBody,
double physicsTime,
bool previousContact,
bool previousOnWalkable,
bool collidedWithEnvironment,
ImmutableArray<uint> collidedObjectIds)
{
EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(owner);
ArgumentNullException.ThrowIfNull(ownerBody);
if (!double.IsFinite(physicsTime)
|| !TryGetCurrentParticipant(owner, ownerBody, out RuntimeEntityKey key))
{
return false;
}
bool reported = false;
if (collidedObjectIds.IsDefault)
collidedObjectIds = ImmutableArray<uint>.Empty;
for (int index = 0; index < collidedObjectIds.Length; index++)
{
if (!IsCurrentParticipant(owner, ownerBody, key))
return reported;
uint collidedId = collidedObjectIds[index];
if (collidedId == 0u || collidedId == key.LocalEntityId)
continue;
if (!_shadows.TryGetCollisionOwner(
collidedId,
out _,
out bool registeredStatic))
{
continue;
}
if (registeredStatic)
{
reported |= ReportEnvironment(
owner,
ownerBody,
key,
previousContact);
continue;
}
if (!TryGetCurrentParticipant(
collidedId,
out RuntimeEntityRecord target,
out PhysicsBody targetBody,
out RuntimeEntityKey targetKey))
{
continue;
}
// The shadow registry proves exact collision ownership/static
// classification only. Retail evaluates every dynamic behavior
// bit from the live target CPhysicsObj, whose state may already
// be newer than a fallible presentation/shadow acknowledgement.
PhysicsStateFlags targetState = targetBody.State;
if ((targetState & PhysicsStateFlags.Static) != 0)
{
reported |= ReportEnvironment(
owner,
ownerBody,
key,
previousContact);
continue;
}
OwnerState ownerState = GetOrCreateOwner(key);
bool isNew = !ownerState.Records.ContainsKey(targetKey);
ownerState.Records[targetKey] = new CollisionRecord(
physicsTime,
(targetState & PhysicsStateFlags.Ethereal) != 0,
target.ServerGuid);
if (!isNew)
continue;
ownerState.Order.Add(targetKey);
AddReverseOwner(targetKey, key);
reported |= ReportObject(
owner,
ownerBody,
key,
target,
targetBody,
targetKey,
targetState,
previousContact);
}
if (!IsCurrentParticipant(owner, ownerBody, key))
return reported;
EndExpiredObjectCollisions(
owner,
ownerBody,
key,
physicsTime,
force: false);
if (!IsCurrentParticipant(owner, ownerBody, key))
return reported;
OwnerState? retained = TryGetOwner(key);
if (retained?.CollidingWithEnvironment == true)
{
retained.CollidingWithEnvironment = collidedWithEnvironment;
}
else if (collidedWithEnvironment
|| (!previousOnWalkable && ownerBody.OnWalkable))
{
reported |= ReportEnvironment(
owner,
ownerBody,
key,
previousContact);
}
TrimEmptyOwner(key);
return reported;
}
/// <summary>
/// Retail leave-world/teleport/Hidden edge: force-end this object's own
/// collision table. Incoming peer records and the environment latch are
/// intentionally retained, matching retail object lookup/lifetime rules.
/// </summary>
internal void LeaveWorld(RuntimeEntityRecord record)
{
EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(record);
if (record.Key is not { } key)
return;
if (!_admissionBlocked.Add(key))
return;
try
{
ForceEnd(record, key);
}
finally
{
_admissionBlocked.Remove(key);
}
}
/// <summary>
/// Session teardown admission transaction. Every exact owner is blocked
/// before the first force-end callback, so a later owner's callback cannot
/// recreate an earlier owner's contact table.
/// </summary>
internal void LeaveWorldBatch(
IReadOnlyList<RuntimeEntityRecord> records)
{
EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(records);
var blocked = new List<(RuntimeEntityRecord Record, RuntimeEntityKey Key)>(
records.Count);
for (int index = 0; index < records.Count; index++)
{
if (records[index].Key is { } key
&& _admissionBlocked.Add(key))
{
blocked.Add((records[index], key));
}
}
try
{
for (int index = 0; index < blocked.Count; index++)
ForceEnd(blocked[index].Record, blocked[index].Key);
}
finally
{
for (int index = 0; index < blocked.Count; index++)
_admissionBlocked.Remove(blocked[index].Key);
}
}
/// <summary>
/// Destruction edge. Retail first force-ends the departing object's own
/// table and then destroys its state. Other owners retain exact-key
/// records until their own expiry/force pass; they then emit the
/// missing-target self-only end with the preserved server GUID.
/// </summary>
internal void Forget(RuntimeEntityRecord record)
{
EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(record);
if (record.Key is not { } key)
return;
// 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
// publish its own retail collision-end suffix before its table is
// forgotten; only an owner already inside ForceEnd is recursive.
if (_admissionBlocked.Contains(key))
{
if (!_leaving.Contains(key))
ForceEnd(record, key);
}
else
{
LeaveWorld(record);
}
_owners.Remove(key);
}
internal void ResetSession()
{
EnsureNotDisposed();
_owners.Clear();
_ownersByPeer.Clear();
_pendingReports.Clear();
_leaving.Clear();
_admissionBlocked.Clear();
_dispatchEpoch = checked(_dispatchEpoch + 1UL);
}
public void Dispose()
{
if (_disposed)
return;
_owners.Clear();
_ownersByPeer.Clear();
_pendingReports.Clear();
_leaving.Clear();
_admissionBlocked.Clear();
_observers = [];
_dispatchEpoch = checked(_dispatchEpoch + 1UL);
_disposed = true;
}
private bool ReportObject(
RuntimeEntityRecord owner,
PhysicsBody ownerBody,
RuntimeEntityKey ownerKey,
RuntimeEntityRecord target,
PhysicsBody targetBody,
RuntimeEntityKey targetKey,
PhysicsStateFlags targetState,
bool previousContact)
{
if ((targetState & PhysicsStateFlags.ReportAsEnvironment) != 0)
{
return ReportEnvironment(
owner,
ownerBody,
ownerKey,
previousContact);
}
PhysicsStateFlags ownerState = ownerBody.State;
bool ownerWasMissile =
(ownerState & PhysicsStateFlags.Missile) != 0;
bool ownerReported = (targetState
& PhysicsStateFlags.IgnoreCollisions) == 0
&& (ownerState & PhysicsStateFlags.ReportCollisions) != 0;
if (ownerReported)
{
Publish(new RuntimeCollisionReport(
NextSequence(),
RuntimeCollisionReportKind.ObjectCollision,
ownerKey,
owner.ServerGuid,
targetKey,
target.ServerGuid,
previousContact,
targetBody.InContact));
}
if (ownerWasMissile
&& (targetState & PhysicsStateFlags.IgnoreCollisions) == 0)
{
StopMissile(
owner,
ownerBody,
ownerKey,
requireCurrentMissile: false);
}
// Retail reads reciprocal eligibility after the source callback. A
// reentrant state update can therefore suppress this second report.
bool targetReported = IsCurrentParticipant(target, targetBody, targetKey)
&& (targetBody.State & PhysicsStateFlags.ReportCollisions) != 0
&& IsCurrentParticipant(owner, ownerBody, ownerKey)
&& (ownerBody.State & PhysicsStateFlags.IgnoreCollisions) == 0;
if (targetReported)
{
Publish(new RuntimeCollisionReport(
NextSequence(),
RuntimeCollisionReportKind.ObjectCollision,
targetKey,
target.ServerGuid,
ownerKey,
owner.ServerGuid,
targetBody.InContact,
previousContact));
}
return ownerReported || targetReported;
}
private bool ReportEnvironment(
RuntimeEntityRecord owner,
PhysicsBody ownerBody,
RuntimeEntityKey ownerKey,
bool previousContact)
{
OwnerState state = GetOrCreateOwner(ownerKey);
if (state.CollidingWithEnvironment)
return false;
bool reported = (ownerBody.State
& PhysicsStateFlags.ReportCollisions) != 0;
state.CollidingWithEnvironment = true;
if (reported)
{
Publish(new RuntimeCollisionReport(
NextSequence(),
RuntimeCollisionReportKind.EnvironmentCollision,
ownerKey,
owner.ServerGuid,
Other: null,
OtherServerGuid: null,
previousContact,
OtherWasInContact: false));
}
StopMissile(owner, ownerBody, ownerKey);
return reported;
}
private void EndExpiredObjectCollisions(
RuntimeEntityRecord owner,
PhysicsBody? ownerBody,
RuntimeEntityKey ownerKey,
double physicsTime,
bool force)
{
if (!_owners.TryGetValue(ownerKey, out OwnerState? state)
|| state.Records.Count == 0)
{
return;
}
List<EndedCollision>? ended = null;
for (int index = 0; index < state.Order.Count; index++)
{
RuntimeEntityKey targetKey = state.Order[index];
if (!state.Records.TryGetValue(
targetKey,
out CollisionRecord collision))
{
continue;
}
double age = physicsTime - collision.TouchedTime;
if (!force
&& !(age > 1d)
&& !(collision.Ethereal && age > 0d))
{
continue;
}
(ended ??= []).Add(new EndedCollision(
targetKey,
collision.ServerGuid));
}
if (ended is null)
return;
// Retail deletes the complete expired set before issuing any end
// callback. Precommit every Runtime index for reentrant safety too.
ulong reportEpoch = _dispatchEpoch;
for (int index = 0; index < ended.Count; index++)
{
EndedCollision collision = ended[index];
state.Records.Remove(collision.Key);
state.Order.Remove(collision.Key);
RemoveReverseOwner(collision.Key, ownerKey);
}
// One accepted delete/replace invalidates the complete precollected
// suffix. Capture the source token once for the whole callback loop;
// recapturing it per peer would let a retained teardown sidecar emit
// a second source report after the first callback accepted deletion.
ulong sourceSessionVersion = _entities.SessionLifetimeVersion;
ulong sourceLifetimeMutation =
_entities.CurrentLifetimeMutation(owner.ServerGuid);
for (int index = 0; index < ended.Count; index++)
{
if (_disposed
|| reportEpoch != _dispatchEpoch
|| _entities.SessionLifetimeVersion != sourceSessionVersion
|| _entities.CurrentLifetimeMutation(owner.ServerGuid)
!= sourceLifetimeMutation)
{
break;
}
EndedCollision collision = ended[index];
if (TryGetParticipant(
collision.Key,
out RuntimeEntityRecord target,
out PhysicsBody targetBody))
{
// report_object_collision_end returns immediately for a
// resolved ReportAsEnvironment target: neither side gets an
// object-end callback.
if ((targetBody.State
& PhysicsStateFlags.ReportAsEnvironment) != 0)
{
continue;
}
PublishResolvedObjectEnd(
owner,
ownerBody,
ownerKey,
target,
targetBody,
collision.Key);
}
else
{
PublishMissingObjectEnd(
owner,
ownerBody,
ownerKey,
collision.Key,
collision.ServerGuid);
}
}
TrimEmptyOwner(ownerKey);
}
private void PublishResolvedObjectEnd(
RuntimeEntityRecord owner,
PhysicsBody? ownerBody,
RuntimeEntityKey ownerKey,
RuntimeEntityRecord target,
PhysicsBody targetBody,
RuntimeEntityKey targetKey)
{
ulong sourceSessionVersion = _entities.SessionLifetimeVersion;
ulong sourceLifetimeMutation =
_entities.CurrentLifetimeMutation(owner.ServerGuid);
ulong reportEpoch = _dispatchEpoch;
if (ownerBody is not null
&& (ownerBody.State & PhysicsStateFlags.ReportCollisions) != 0
&& IsKnownParticipant(owner, ownerBody, ownerKey))
{
Publish(new RuntimeCollisionReport(
NextSequence(),
RuntimeCollisionReportKind.ObjectCollisionEnd,
ownerKey,
owner.ServerGuid,
targetKey,
target.ServerGuid,
ownerBody.InContact,
targetBody.InContact));
}
if ((targetBody.State & PhysicsStateFlags.ReportCollisions) != 0
&& reportEpoch == _dispatchEpoch
&& !_disposed
&& _entities.SessionLifetimeVersion == sourceSessionVersion
&& _entities.CurrentLifetimeMutation(owner.ServerGuid)
== sourceLifetimeMutation
&& ownerBody is not null
&& IsKnownParticipant(owner, ownerBody, ownerKey)
&& IsKnownParticipant(target, targetBody, targetKey))
{
Publish(new RuntimeCollisionReport(
NextSequence(),
RuntimeCollisionReportKind.ObjectCollisionEnd,
targetKey,
target.ServerGuid,
ownerKey,
owner.ServerGuid,
targetBody.InContact,
ownerBody?.InContact ?? false));
}
}
private void PublishMissingObjectEnd(
RuntimeEntityRecord owner,
PhysicsBody? ownerBody,
RuntimeEntityKey ownerKey,
RuntimeEntityKey targetKey,
uint targetServerGuid)
{
if (ownerBody is null
|| (ownerBody.State & PhysicsStateFlags.ReportCollisions) == 0
|| !IsKnownParticipant(owner, ownerBody, ownerKey))
{
return;
}
Publish(new RuntimeCollisionReport(
NextSequence(),
RuntimeCollisionReportKind.ObjectCollisionEnd,
ownerKey,
owner.ServerGuid,
targetKey,
targetServerGuid,
ownerBody.InContact,
OtherWasInContact: false));
}
private void StopMissile(
RuntimeEntityRecord owner,
PhysicsBody ownerBody,
RuntimeEntityKey ownerKey,
bool requireCurrentMissile = true)
{
if (!IsCurrentParticipant(owner, ownerBody, ownerKey)
|| !_entities.StopMissileAfterCollision(
owner,
requireCurrentMissile))
{
return;
}
_shadows.UpdatePhysicsState(
ownerKey.LocalEntityId,
(uint)owner.FinalPhysicsState);
}
private OwnerState GetOrCreateOwner(RuntimeEntityKey key)
{
if (!_owners.TryGetValue(key, out OwnerState? owner))
{
owner = new OwnerState();
_owners.Add(key, owner);
}
return owner;
}
private OwnerState? TryGetOwner(RuntimeEntityKey key) =>
_owners.TryGetValue(key, out OwnerState? owner) ? owner : null;
private void TrimEmptyOwner(RuntimeEntityKey key)
{
if (_owners.TryGetValue(key, out OwnerState? owner)
&& owner.Records.Count == 0
&& !owner.CollidingWithEnvironment)
{
_owners.Remove(key);
}
}
private void AddReverseOwner(
RuntimeEntityKey peer,
RuntimeEntityKey owner)
{
if (!_ownersByPeer.TryGetValue(
peer,
out List<RuntimeEntityKey>? owners))
{
owners = [];
_ownersByPeer.Add(peer, owners);
}
if (!owners.Contains(owner))
owners.Add(owner);
}
private void RemoveReverseOwner(
RuntimeEntityKey peer,
RuntimeEntityKey owner)
{
if (!_ownersByPeer.TryGetValue(
peer,
out List<RuntimeEntityKey>? owners))
{
return;
}
owners.Remove(owner);
if (owners.Count == 0)
_ownersByPeer.Remove(peer);
}
private bool TryGetCurrentParticipant(
RuntimeEntityRecord record,
PhysicsBody body,
out RuntimeEntityKey key)
{
key = record.Key ?? default;
return key != default
&& IsCurrentParticipant(record, body, key);
}
private bool TryGetCurrentParticipant(
uint localEntityId,
out RuntimeEntityRecord record,
out PhysicsBody body,
out RuntimeEntityKey key)
{
if (_entities.TryGetByLocalId(localEntityId, out record!)
&& record.PhysicsBody is { } retained
&& record.Key is { } retainedKey
&& retainedKey.LocalEntityId == localEntityId
&& !_leaving.Contains(retainedKey)
&& !_admissionBlocked.Contains(retainedKey)
&& retained.InWorld
&& (retained.State & PhysicsStateFlags.Hidden) == 0
&& _entities.IsCurrent(record))
{
body = retained;
key = retainedKey;
return true;
}
body = null!;
key = default;
return false;
}
private bool TryGetParticipant(
RuntimeEntityKey key,
out RuntimeEntityRecord record,
out PhysicsBody body)
{
if (_entities.TryGetByLocalId(key.LocalEntityId, out record!)
&& record.Key == key
&& !_leaving.Contains(key)
&& _entities.IsCurrent(record)
&& record.PhysicsBody is { } retained)
{
body = retained;
return true;
}
body = null!;
return false;
}
private bool IsCurrentParticipant(
RuntimeEntityRecord record,
PhysicsBody body,
RuntimeEntityKey key) =>
_entities.IsCurrent(record)
&& !_leaving.Contains(key)
&& !_admissionBlocked.Contains(key)
&& body.InWorld
&& (body.State & PhysicsStateFlags.Hidden) == 0
&& IsKnownParticipant(record, body, key);
private void ForceEnd(
RuntimeEntityRecord record,
RuntimeEntityKey key)
{
if (!_leaving.Add(key))
return;
try
{
EndExpiredObjectCollisions(
record,
record.PhysicsBody,
key,
physicsTime: 0d,
force: true);
}
finally
{
_leaving.Remove(key);
}
}
private bool IsKnownParticipant(
RuntimeEntityRecord record,
PhysicsBody body,
RuntimeEntityKey key) =>
record.Key == key
&& ReferenceEquals(record.PhysicsBody, body)
&& _entities.TryGetByLocalId(
key.LocalEntityId,
out RuntimeEntityRecord retained)
&& ReferenceEquals(retained, record);
private ulong NextSequence() => checked(++_nextSequence);
private void Publish(in RuntimeCollisionReport report)
{
_pendingReports.Enqueue(new PendingReport(_dispatchEpoch, report));
if (_dispatching)
return;
_dispatching = true;
try
{
while (!_disposed && _pendingReports.TryDequeue(out PendingReport pending))
{
if (pending.Epoch != _dispatchEpoch)
continue;
IRuntimeCollisionReportObserver[] observers = _observers;
for (int index = 0; index < observers.Length; index++)
{
try
{
observers[index].OnCollisionReport(pending.Report);
}
catch (Exception error)
{
_dispatchFailureCount++;
System.Diagnostics.Trace.TraceError(
"Runtime collision-report observer failed: {0}",
error);
}
if (_disposed || pending.Epoch != _dispatchEpoch)
break;
}
}
}
finally
{
_dispatching = false;
if (_disposed)
_pendingReports.Clear();
}
}
private void Unsubscribe(IRuntimeCollisionReportObserver observer)
{
int index = Array.IndexOf(_observers, observer);
if (index < 0)
return;
if (_observers.Length == 1)
{
_observers = [];
return;
}
var replacement = new IRuntimeCollisionReportObserver[
_observers.Length - 1];
if (index > 0)
Array.Copy(_observers, 0, replacement, 0, index);
if (index < _observers.Length - 1)
{
Array.Copy(
_observers,
index + 1,
replacement,
index,
_observers.Length - index - 1);
}
_observers = replacement;
}
private void EnsureNotDisposed() =>
ObjectDisposedException.ThrowIf(_disposed, this);
private sealed class OwnerState
{
internal Dictionary<RuntimeEntityKey, CollisionRecord> Records { get; }
= new();
internal List<RuntimeEntityKey> Order { get; } = [];
internal bool CollidingWithEnvironment { get; set; }
}
private readonly record struct CollisionRecord(
double TouchedTime,
bool Ethereal,
uint ServerGuid);
private readonly record struct EndedCollision(
RuntimeEntityKey Key,
uint ServerGuid);
private readonly record struct PendingReport(
ulong Epoch,
RuntimeCollisionReport Report);
private sealed class Subscription : IDisposable
{
private RuntimeCollisionReportingState? _owner;
private readonly IRuntimeCollisionReportObserver _observer;
internal Subscription(
RuntimeCollisionReportingState owner,
IRuntimeCollisionReportObserver observer)
{
_owner = owner;
_observer = observer;
}
public void Dispose()
{
RuntimeCollisionReportingState? owner =
Interlocked.Exchange(ref _owner, null);
owner?.Unsubscribe(_observer);
}
}
}

View file

@ -23,6 +23,14 @@ public readonly record struct RuntimePhysicsOwnershipSnapshot(
int UnboundDeferredSetPositionCellCount,
int UnboundDeferredSetPositionCellOrderCount,
int PreparedSetPositionMoverCount,
int CollisionReportOwnerCount,
int TrackedCollisionObjectCount,
int CollisionReportReversePeerCount,
int CollisionReportObserverCount,
int PendingCollisionReportCount,
int LeavingCollisionReportOwnerCount,
int CollisionReportAdmissionBlockedOwnerCount,
bool IsCollisionReportDispatching,
int CollisionAdmissionCount,
int CollisionGenerationCount,
bool OwnsProductionDataCache,
@ -49,6 +57,14 @@ public readonly record struct RuntimePhysicsOwnershipSnapshot(
&& UnboundDeferredSetPositionCellCount == 0
&& UnboundDeferredSetPositionCellOrderCount == 0
&& PreparedSetPositionMoverCount == 0
&& CollisionReportOwnerCount == 0
&& TrackedCollisionObjectCount == 0
&& CollisionReportReversePeerCount == 0
&& CollisionReportObserverCount == 0
&& PendingCollisionReportCount == 0
&& LeavingCollisionReportOwnerCount == 0
&& CollisionReportAdmissionBlockedOwnerCount == 0
&& !IsCollisionReportDispatching
&& CollisionAdmissionCount == 0
&& CollisionGenerationCount == 0
&& OwnsProductionDataCache;
@ -1063,6 +1079,9 @@ public sealed class RuntimePhysicsState : IDisposable
Engine.ShadowObjects.OwnerMutated += OnCollisionOwnerMutated;
Engine.ShadowObjects.OwnerPrefixMembershipChanged +=
OnCollisionOwnerPrefixMembershipChanged;
CollisionReports = new RuntimeCollisionReportingState(
Entities,
Engine.ShadowObjects);
SetPosition = new RuntimeSetPositionState(this, Entities);
}
@ -1082,12 +1101,16 @@ public sealed class RuntimePhysicsState : IDisposable
Engine.ShadowObjects.OwnerMutated += OnCollisionOwnerMutated;
Engine.ShadowObjects.OwnerPrefixMembershipChanged +=
OnCollisionOwnerPrefixMembershipChanged;
CollisionReports = new RuntimeCollisionReportingState(
Entities,
Engine.ShadowObjects);
SetPosition = new RuntimeSetPositionState(this, Entities);
}
internal RuntimeEntityDirectory Entities { get; }
public PhysicsEngine Engine { get; }
public PhysicsDataCache DataCache { get; }
internal RuntimeCollisionReportingState CollisionReports { get; }
internal RuntimeSetPositionState SetPosition { get; }
public int SpatialRootCount => _spatialRoots.Count;
public int SpatialRemoteCount => _spatialRemotes.Count;
@ -1107,6 +1130,8 @@ public sealed class RuntimePhysicsState : IDisposable
{
RuntimeSetPositionOwnershipSnapshot setPosition =
SetPosition.CaptureOwnership();
RuntimeCollisionReportingOwnershipSnapshot collisionReports =
CollisionReports.CaptureOwnership();
return new(
Engine.LandblockCount,
Engine.ShadowObjects.RetainedRegistrationCount,
@ -1127,6 +1152,14 @@ public sealed class RuntimePhysicsState : IDisposable
setPosition.UnboundDeferredCellCount,
setPosition.UnboundDeferredCellOrderCount,
setPosition.PreparedMoverCount,
collisionReports.OwnerCount,
collisionReports.TrackedObjectCount,
collisionReports.ReversePeerCount,
collisionReports.ObserverCount,
collisionReports.PendingReportCount,
collisionReports.LeavingOwnerCount,
collisionReports.AdmissionBlockedOwnerCount,
collisionReports.IsDispatching,
_collisionAdmissions.Count,
_collisionGenerations.Count,
ReferenceEquals(Engine.DataCache, DataCache),
@ -2303,6 +2336,7 @@ public sealed class RuntimePhysicsState : IDisposable
}
_preparedCollisionGenerations.Clear();
SetPosition.Dispose();
CollisionReports.Dispose();
_collisionOwnerJournal.Clear();
_collisionOwnerSubscribers.Clear();
Engine.Clear();
@ -2511,6 +2545,7 @@ public sealed class RuntimePhysicsState : IDisposable
ulong positionAuthorityVersion,
ulong spatialAuthorityVersion,
ulong velocityAuthorityVersion,
double physicsTime,
bool previousContact,
bool previousOnWalkable,
in PhysicsSetPositionCollisionReport report)
@ -2518,22 +2553,39 @@ public sealed class RuntimePhysicsState : IDisposable
if (!Entities.IsCurrent(record)
|| record.PositionAuthorityVersion != positionAuthorityVersion
|| record.SpatialAuthorityVersion != spatialAuthorityVersion
|| (velocityAuthorityVersion != 0UL
&& record.VelocityAuthorityVersion
!= velocityAuthorityVersion)
|| record.PhysicsBody is not { } body)
{
return false;
}
_ = previousContact;
_ = previousOnWalkable;
bool current = HandleSetPositionCollisionReports(
record,
positionAuthorityVersion,
spatialAuthorityVersion,
physicsTime,
previousContact: false,
previousOnWalkable: false,
collidedWithEnvironment: report.CollidedWithEnvironment,
collidedObjectIds: report.CollidedObjectIds,
collisionHandlerResult: out bool collisionHandlerResult);
if (!current)
return collisionHandlerResult;
if (velocityAuthorityVersion != 0UL
&& record.VelocityAuthorityVersion != velocityAuthorityVersion)
{
return collisionHandlerResult;
}
body.FramesStationaryFall = report.FramesStationaryFall;
PhysicsObjUpdate.HandleAllCollisions(
body,
report.CollisionNormalValid,
report.CollisionNormal,
previousContact,
previousOnWalkable,
body.OnWalkable);
prevContact: false,
prevOnWalkable: false,
nowOnWalkable: body.OnWalkable);
body.TransientState &= ~(TransientStateFlags.StationaryFall
| TransientStateFlags.StationaryStop
| TransientStateFlags.StationaryStuck);
@ -2544,12 +2596,49 @@ public sealed class RuntimePhysicsState : IDisposable
3 => TransientStateFlags.StationaryStuck,
_ => TransientStateFlags.None,
};
// Retail returns the result of collision reporting, not a collision-
// presence guess. Runtime does not yet own the per-object report/
// tracking table required to reproduce that return value, so fail
// closed. This preserves ordinary placement rejection and leaves the
// already-registered reporting seam explicit for the 4B2 cutover.
return false;
return collisionHandlerResult;
}
/// <summary>
/// Runs only retail's report/tracking half of handle_all_collisions. A
/// successful SetPosition invokes this after contact/ground callbacks and
/// before physical response and shadow reflood; invalid placement invokes
/// it before its one response pass.
/// </summary>
internal bool HandleSetPositionCollisionReports(
RuntimeEntityRecord record,
ulong positionAuthorityVersion,
ulong spatialAuthorityVersion,
double physicsTime,
bool previousContact,
bool previousOnWalkable,
bool collidedWithEnvironment,
System.Collections.Immutable.ImmutableArray<uint> collidedObjectIds,
out bool collisionHandlerResult)
{
collisionHandlerResult = false;
if (!Entities.IsCurrent(record)
|| record.PositionAuthorityVersion != positionAuthorityVersion
|| record.SpatialAuthorityVersion != spatialAuthorityVersion
|| record.PhysicsBody is not { } body)
{
return false;
}
collisionHandlerResult = CollisionReports.HandleReports(
record,
body,
physicsTime,
previousContact,
previousOnWalkable,
collidedWithEnvironment,
collidedObjectIds);
return Entities.IsCurrent(record)
&& record.PositionAuthorityVersion == positionAuthorityVersion
&& record.SpatialAuthorityVersion == spatialAuthorityVersion
&& ReferenceEquals(record.PhysicsBody, body)
&& body.InWorld
&& (body.State & PhysicsStateFlags.Hidden) == 0;
}
private void OnCollisionOwnerMutated(uint ownerId, ulong version)

View file

@ -213,6 +213,25 @@ internal sealed class RuntimeSetPositionState : IDisposable
}
}
private sealed class ContactCommitGuard(
RuntimeSetPositionState owner,
Operation operation,
RuntimeEntityRecord record,
PhysicsBody body,
ulong placementCommitVersion,
uint fullCellId)
{
internal bool IsCurrent() =>
owner.IsCanonicalPlacementCommitCurrent(
operation,
record,
body,
placementCommitVersion,
fullCellId,
requireSpatialRoot: false)
&& owner.IsCollisionReportingEligible(record, body);
}
private readonly RuntimePhysicsState _physics;
private readonly RuntimeEntityDirectory _entities;
private readonly Dictionary<RuntimeEntityKey, Operation> _operations = [];
@ -549,9 +568,12 @@ internal sealed class RuntimeSetPositionState : IDisposable
operation.PositionAuthorityVersion,
operation.SourceSpatialAuthorityVersion,
operation.SourceVelocityAuthorityVersion,
canonicalCommand.GameTime,
operation.PreviousContact,
operation.PreviousOnWalkable,
report));
if (!IsCurrent(operation))
return Outcome(RuntimeSetPositionStatus.Cancelled, result, default);
operation.Result = result;
if (!result.IsSuccessful)
{
@ -1236,11 +1258,14 @@ internal sealed class RuntimeSetPositionState : IDisposable
operation.PositionAuthorityVersion,
operation.SpatialAuthorityVersion,
operation.SourceVelocityAuthorityVersion,
operation.Command.GameTime,
operation.PreviousContact,
operation.PreviousOnWalkable,
report))
: InvalidResult(operation.Command.Physics);
operation.CollisionGenerationReady = false;
if (!IsCurrent(operation))
return;
if (result.IsDeferred)
{
operation.Result = result;
@ -1320,18 +1345,6 @@ internal sealed class RuntimeSetPositionState : IDisposable
body.TransientState |= TransientStateFlags.Sliding;
else
body.TransientState &= ~TransientStateFlags.Sliding;
body.FramesStationaryFall = result.FramesStationaryFall;
body.TransientState &= ~(TransientStateFlags.StationaryFall
| TransientStateFlags.StationaryStop
| TransientStateFlags.StationaryStuck);
body.TransientState |= result.FramesStationaryFall switch
{
1 => TransientStateFlags.StationaryFall,
2 => TransientStateFlags.StationaryStop,
3 => TransientStateFlags.StationaryStuck,
_ => TransientStateFlags.None,
};
IRuntimeRemotePlacement? remote =
record.RemoteMotion as IRuntimeRemotePlacement;
if (record.FullCellId != result.CellId)
@ -1354,6 +1367,109 @@ internal sealed class RuntimeSetPositionState : IDisposable
remote.LastShadowSyncOrientation = result.Orientation;
}
uint committedCellId = result.CellId;
bool collidedWithEnvironment = result.CollidedWithEnvironment;
System.Collections.Immutable.ImmutableArray<uint> collidedObjectIds =
result.CollidedObjectIds;
if (!IsCanonicalPlacementCommitCurrent(
operation,
record,
body,
canonicalCommitVersion,
committedCellId,
requireSpatialRoot: false))
return false;
bool contactCommitted;
if (remote is null)
{
contactCommitted = PhysicsObjUpdate.CommitSetPositionContactTransition(
body,
result.InContact,
result.OnWalkable,
operation.PreviousOnWalkable);
}
else
{
var guard = new ContactCommitGuard(
this,
operation,
record,
body,
canonicalCommitVersion,
committedCellId);
contactCommitted = PhysicsObjUpdate.CommitSetPositionContactTransition(
body,
result.InContact,
result.OnWalkable,
operation.PreviousOnWalkable,
remote.HitGround,
remote.LeaveGround,
guard.IsCurrent);
}
if (!contactCommitted
|| !IsCanonicalPlacementCommitCurrent(
operation,
record,
body,
canonicalCommitVersion,
committedCellId,
requireSpatialRoot: false)
|| !IsCollisionReportingEligible(record, body))
{
return false;
}
bool reportingCurrent = _physics.HandleSetPositionCollisionReports(
record,
operation.PositionAuthorityVersion,
operation.SpatialAuthorityVersion,
operation.Command.GameTime,
operation.PreviousContact,
operation.PreviousOnWalkable,
collidedWithEnvironment,
collidedObjectIds,
out _);
if (!reportingCurrent
|| !IsCanonicalPlacementCommitCurrent(
operation,
record,
body,
canonicalCommitVersion,
committedCellId,
requireSpatialRoot: false)
|| !IsCollisionReportingEligible(record, body))
return false;
body.FramesStationaryFall = result.FramesStationaryFall;
if (IsVelocityCurrent(operation))
{
PhysicsObjUpdate.HandleAllCollisions(
body,
result.CollisionNormalValid,
result.CollisionNormal,
operation.PreviousContact,
operation.PreviousOnWalkable,
body.OnWalkable);
}
body.TransientState &= ~(TransientStateFlags.StationaryFall
| TransientStateFlags.StationaryStop
| TransientStateFlags.StationaryStuck);
body.TransientState |= result.FramesStationaryFall switch
{
1 => TransientStateFlags.StationaryFall,
2 => TransientStateFlags.StationaryStop,
3 => TransientStateFlags.StationaryStuck,
_ => TransientStateFlags.None,
};
if (remote is not null)
remote.Airborne = !body.OnWalkable;
if (!IsCanonicalPlacementCommitCurrent(
operation,
record,
body,
canonicalCommitVersion,
committedCellId,
requireSpatialRoot: false))
return false;
_physics.Engine.ShadowObjects.CommitSetPosition(
operation.Key.LocalEntityId,
result.Position,
@ -1370,39 +1486,44 @@ internal sealed class RuntimeSetPositionState : IDisposable
operation.EnteringWorldFromCelllessResidence = false;
CancelLostFamilyDeadlines(operation);
uint committedCellId = result.CellId;
bool IsCanonicalCommitCurrent() =>
_entities.IsCurrent(record)
&& ReferenceEquals(record.PhysicsBody, body)
&& record.PlacementCommitVersion == canonicalCommitVersion
&& record.FullCellId == committedCellId
&& _physics.IsSpatialRoot(record);
Action? hitGround = remote is null ? null : remote.HitGround;
Action? leaveGround = remote is null ? null : remote.LeaveGround;
if (!PhysicsObjUpdate.CommitSetPositionTransition(
body,
result.InContact,
result.OnWalkable,
result.CollisionNormalValid,
result.CollisionNormal,
operation.PreviousContact,
operation.PreviousOnWalkable,
hitGround,
leaveGround,
IsCanonicalCommitCurrent,
() => IsCanonicalCommitCurrent()
&& IsVelocityCurrent(operation)))
{
return false;
}
if (remote is not null)
remote.Airborne = !body.OnWalkable;
return IsCurrent(operation)
&& IsCanonicalCommitCurrent();
&& IsCanonicalPlacementCommitCurrent(
operation,
record,
body,
canonicalCommitVersion,
committedCellId,
requireSpatialRoot: true);
}
private bool IsCanonicalPlacementCommitCurrent(
Operation operation,
RuntimeEntityRecord record,
PhysicsBody body,
ulong placementCommitVersion,
uint fullCellId,
bool requireSpatialRoot) =>
_entities.IsCurrent(record)
&& ReferenceEquals(record.PhysicsBody, body)
&& record.PositionAuthorityVersion
== operation.PositionAuthorityVersion
&& record.SpatialAuthorityVersion
== operation.SpatialAuthorityVersion
&& record.PlacementCommitVersion == placementCommitVersion
&& record.FullCellId == fullCellId
&& (!requireSpatialRoot || _physics.IsSpatialRoot(record));
private bool IsCollisionReportingEligible(
RuntimeEntityRecord record,
PhysicsBody body) =>
_entities.IsCurrent(record)
&& ReferenceEquals(record.PhysicsBody, body)
&& body.InWorld
&& (body.State & PhysicsStateFlags.Hidden) == 0;
private void WithdrawCanonical(RuntimeEntityRecord record)
{
_physics.CollisionReports.LeaveWorld(record);
_physics.RemoveSpatialProjection(record);
if (record.Key is { } key)
_physics.Engine.ShadowObjects.Suspend(key.LocalEntityId);