feat(runtime): own SetPosition collision reports
This commit is contained in:
parent
ec627c13a2
commit
237d1184d2
19 changed files with 3744 additions and 103 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
918
src/AcDream.Runtime/Physics/RuntimeCollisionReportingState.cs
Normal file
918
src/AcDream.Runtime/Physics/RuntimeCollisionReportingState.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue