refactor(runtime): own projectile simulation

Move projectile component identity, prediction invalidation, spatial worksets, authoritative corrections, and the retail physics step into AcDream.Runtime. Keep App as the DAT-shape and presentation adapter so ACE outcomes and visible behavior remain unchanged.
This commit is contained in:
Erik 2026-07-26 14:17:42 +02:00
parent fce8e7b18e
commit 2aee33569f
19 changed files with 1255 additions and 335 deletions

View file

@ -437,7 +437,6 @@ internal sealed class LivePresentationCompositionPhase
var projectileController = new ProjectileController(
liveEntities,
d.PhysicsEngine,
new DatProjectileSetupResolver(content.Dats, d.DatLock),
new EntityRootPosePublisher(d.EffectPoses),
d.WorldOrigin)

View file

@ -35,51 +35,30 @@ internal sealed class DatProjectileSetupResolver : IProjectileSetupResolver
}
/// <summary>
/// Update-thread presentation owner for live retail missiles. Logical identity,
/// generation, accepted network state, and teardown remain owned by
/// <see cref="LiveEntityRuntime"/>; this controller only advances the
/// <see cref="PhysicsBody"/> attached to that canonical record and commits its
/// frame through the existing <see cref="WorldEntity"/> projection.
/// Update-thread presentation adapter for live retail missiles. Runtime owns
/// logical identity, the projectile component, prediction versions, the
/// canonical body/cell, and the split simulation transaction. This adapter
/// prepares the DAT Setup sphere, preserves animation-hook ordering, and
/// projects committed frames into <see cref="WorldEntity"/> and effect poses.
/// </summary>
/// <remarks>
/// Retail anchors are <c>CPhysicsObj::update_object</c> <c>0x00515D10</c>,
/// <c>CPhysicsObj::UpdateObjectInternal</c> <c>0x005156B0</c>, and
/// <c>CPhysicsObj::SetPositionInternal</c> <c>0x00515330</c>. The pure
/// integration/sweep is implemented by <see cref="ProjectilePhysicsStepper"/>.
/// integration/sweep is invoked by Runtime through
/// <see cref="RuntimeProjectilePhysicsUpdater"/>.
/// ACE remains authoritative for collision outcomes, damage, effects, and
/// deletion; no such events are synthesized here.
/// </remarks>
internal sealed class ProjectileController
{
internal sealed class Runtime : ILiveEntityProjectileRuntime
{
internal Runtime(
ushort generation,
PhysicsBody body,
ProjectileCollisionSphere collisionSphere)
{
Generation = generation;
Body = body;
CollisionSphere = collisionSphere;
}
internal ushort Generation { get; }
public PhysicsBody Body { get; }
internal ProjectileCollisionSphere CollisionSphere { get; }
internal ulong PredictionAuthorityVersion { get; private set; }
internal void InvalidatePrediction() =>
PredictionAuthorityVersion++;
}
internal readonly record struct QuantumStep(
LiveEntityRecord Record,
Runtime Runtime,
WorldEntity Entity,
ProjectileQuantumPreparation Preparation,
ulong PredictionAuthorityVersion);
RuntimeProjectilePhysicsCommit RuntimeCommit);
private readonly LiveEntityRuntime _liveEntities;
private readonly ProjectilePhysicsStepper _stepper;
private readonly RuntimeProjectilePhysicsUpdater _runtimeUpdater;
private readonly ShadowObjectRegistry _shadows;
private readonly IProjectileSetupResolver? _setupResolver;
private readonly IEntityRootPosePublisher? _rootPoses;
@ -89,15 +68,14 @@ internal sealed class ProjectileController
internal ProjectileController(
LiveEntityRuntime liveEntities,
PhysicsEngine physicsEngine,
IProjectileSetupResolver? setupResolver = null,
IEntityRootPosePublisher? rootPoses = null,
LiveWorldOriginState? origin = null)
{
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
ArgumentNullException.ThrowIfNull(physicsEngine);
_stepper = new ProjectilePhysicsStepper(physicsEngine);
_shadows = physicsEngine.ShadowObjects;
_runtimeUpdater = new RuntimeProjectilePhysicsUpdater(
liveEntities.Physics);
_shadows = liveEntities.Physics.Engine.ShadowObjects;
_setupResolver = setupResolver;
_rootPoses = rootPoses;
_origin = origin;
@ -107,7 +85,7 @@ internal sealed class ProjectileController
internal Action<string>? DiagnosticSink { get; set; }
internal int Count => _liveEntities.Records.Count(
static record => record.ProjectileRuntime is Runtime);
static record => record.ProjectileRuntime is not null);
internal bool CanAcceptVectorPayload(
uint serverGuid,
@ -167,7 +145,7 @@ internal sealed class ProjectileController
|| (record.FinalPhysicsState & PhysicsStateFlags.Missile) == 0)
return false;
if (record.ProjectileRuntime is Runtime retained)
if (record.ProjectileRuntime is RuntimeProjectile retained)
{
retained.Body.State = record.FinalPhysicsState;
return true;
@ -261,7 +239,7 @@ internal sealed class ProjectileController
// Claim the incarnation's one CPhysicsObj before Rebucket can
// publish visibility callbacks. A callback that also needs a body
// must observe and adopt this same instance, never race a private
// projectile candidate into SetProjectileRuntime.
// projectile candidate into Runtime's canonical component slot.
body = _liveEntities.GetOrCreatePhysicsBody(
record.ServerGuid,
_ =>
@ -308,7 +286,7 @@ internal sealed class ProjectileController
// the one projectile component before this outer call resumes. Adopt
// that component; never replace it with a second Runtime that merely
// happens to share the canonical CPhysicsObj body.
if (currentRecord.ProjectileRuntime is Runtime concurrentRuntime)
if (currentRecord.ProjectileRuntime is RuntimeProjectile concurrentRuntime)
return ReferenceEquals(concurrentRuntime.Body, body);
// A newer Position can legally arrive from a projection callback
@ -317,11 +295,14 @@ internal sealed class ProjectileController
// canonical body frame, never the pre-callback cell captured above.
canonicalCellId = body.CellPosition.ObjCellId;
var runtime = new Runtime(
record.Generation,
body,
sphere);
_liveEntities.SetProjectileRuntime(record.ServerGuid, runtime);
RuntimeProjectile runtime = (RuntimeProjectile)
_liveEntities.BindProjectileRuntime(
record.ServerGuid,
body,
sphere,
() => _liveEntities.IsCurrentRecord(record)
&& ReferenceEquals(record.WorldEntity, entity)
&& ReferenceEquals(record.PhysicsBody, body));
if (HasVisibleCell(record) && !IsHidden(record))
{
@ -374,7 +355,7 @@ internal sealed class ProjectileController
{
ArgumentNullException.ThrowIfNull(expectedRecord);
uint serverGuid = expectedRecord.ServerGuid;
if (!TryGetCurrent(serverGuid, out LiveEntityRecord record, out Runtime runtime))
if (!TryGetCurrent(serverGuid, out LiveEntityRecord record, out RuntimeProjectile runtime))
return false;
if (!ReferenceEquals(record, expectedRecord)
|| record.VectorAuthorityVersion != expectedVectorAuthorityVersion
@ -400,17 +381,19 @@ internal sealed class ProjectileController
}
_lastFiniteGameTime = currentTime;
runtime.InvalidatePrediction();
if (!_liveEntities.TryCommitAuthoritativeVector(
record,
runtime.Body,
return _runtimeUpdater.ApplyAuthoritativeVector(
record.Canonical,
expectedVectorAuthorityVersion,
expectedVelocityAuthorityVersion,
velocity,
angularVelocity,
currentTime))
{
return true;
}
return true;
currentTime,
() => TryGetCurrent(
serverGuid,
out LiveEntityRecord current,
out RuntimeProjectile currentRuntime)
&& ReferenceEquals(current, record)
&& ReferenceEquals(currentRuntime, runtime));
}
internal bool ApplyAuthoritativeState(
@ -453,34 +436,22 @@ internal sealed class ProjectileController
DiagnosticSink?.Invoke(
$"Ignored invalid State update clock for live object 0x{serverGuid:X8}; state remains authoritative.");
if (record.ProjectileRuntime is Runtime runtime)
if (record.ProjectileRuntime is RuntimeProjectile runtime)
{
runtime.InvalidatePrediction();
bool wasMissile =
(runtime.Body.State & PhysicsStateFlags.Missile) != 0;
runtime.Body.State = state;
if ((state & PhysicsStateFlags.Missile) != 0 && !wasMissile)
{
// Ownership transfers back from generic remote motion to the
// absolute projectile clock. State alone must not activate,
// but an already-active body keeps moving from now rather
// than replaying the ordinary-motion interval. A malformed
// local receipt clock is not retail state and cannot clear
// Active; translate to the last finite App clock instead.
runtime.Body.LastUpdateTime = effectiveClock;
if (record.FullCellId != 0)
{
runtime.Body.SnapToCell(
record.FullCellId,
runtime.Body.Position,
CellLocalFromWorld(
runtime.Body.Position,
record.FullCellId,
liveCenterX,
liveCenterY));
}
}
return true;
return _runtimeUpdater.ApplyAuthoritativeState(
record.Canonical,
expectedStateAuthorityVersion,
state,
effectiveClock,
liveCenterX,
liveCenterY,
() => _liveEntities.TryGetRecord(
serverGuid,
out LiveEntityRecord current)
&& ReferenceEquals(current, record)
&& ReferenceEquals(
current.ProjectileRuntime,
runtime));
}
// SetState applies to the canonical CPhysicsObj independently of
@ -552,7 +523,7 @@ internal sealed class ProjectileController
{
ArgumentNullException.ThrowIfNull(expectedRecord);
uint serverGuid = expectedRecord.ServerGuid;
if (!TryGetCurrent(serverGuid, out LiveEntityRecord record, out Runtime runtime)
if (!TryGetCurrent(serverGuid, out LiveEntityRecord record, out RuntimeProjectile runtime)
|| !ReferenceEquals(record, expectedRecord)
|| record.PositionAuthorityVersion != expectedPositionAuthorityVersion
|| (record.FinalPhysicsState & PhysicsStateFlags.Missile) == 0
@ -574,76 +545,39 @@ internal sealed class ProjectileController
}
_lastFiniteGameTime = currentTime;
PhysicsBody body = runtime.Body;
runtime.InvalidatePrediction();
bool wasInWorld = body.InWorld;
body.Orientation = orientation;
body.SnapToCell(fullCellId, worldPosition, cellLocalPosition);
body.State = record.FinalPhysicsState;
// PositionPack::UnPack initializes an absent velocity to zero and
// MoveOrTeleport installs that exact vector through set_velocity.
// Apply it to this retained CPhysicsObj before any callback can
// displace the incarnation; a replacement record owns another body.
if (record.VelocityAuthorityVersion == expectedVelocityAuthorityVersion
&& !_liveEntities.TryCommitAuthoritativeVelocity(
record,
body,
velocity,
currentTime))
{
return true;
}
uint canonicalCellId = body.CellPosition.ObjCellId;
entity.SetPosition(worldPosition);
entity.Rotation = orientation;
entity.ParentCellId = canonicalCellId;
if (!_liveEntities.RebucketLiveEntity(serverGuid, canonicalCellId)
|| !TryGetCurrent(serverGuid, out LiveEntityRecord afterRebucket, out Runtime afterRuntime)
|| !ReferenceEquals(afterRebucket, record)
|| !ReferenceEquals(afterRuntime, runtime)
|| !ReferenceEquals(afterRebucket.WorldEntity, entity)
|| afterRebucket.PositionAuthorityVersion
!= expectedPositionAuthorityVersion)
{
// This packet was a projectile correction when admitted. A
// re-entrant observer displaced that incarnation; report handled
// so the generic remote path cannot mutate the replacement.
return true;
}
if (HasVisibleCell(record) && !IsHidden(record))
{
if (!wasInWorld)
bool ExternalOwnerValid() =>
TryGetCurrent(
serverGuid,
out LiveEntityRecord current,
out RuntimeProjectile currentRuntime)
&& ReferenceEquals(current, record)
&& ReferenceEquals(currentRuntime, runtime)
&& ReferenceEquals(current.WorldEntity, entity)
&& current.PositionAuthorityVersion
== expectedPositionAuthorityVersion;
return _runtimeUpdater.ApplyAuthoritativePosition(
record.Canonical,
expectedPositionAuthorityVersion,
expectedVelocityAuthorityVersion,
worldPosition,
cellLocalPosition,
orientation,
velocity,
fullCellId,
currentTime,
liveCenterX,
liveCenterY,
snapshot =>
{
// prepare_to_enter_world rebases the CPhysicsObj timestamp in
// the same operation that restores Active. The record clock is
// canonical, but keep this legacy body field synchronized for
// the absolute-time Core API and diagnostics.
body.LastUpdateTime = currentTime;
Activate(body, currentTime);
}
body.InWorld = true;
ShadowPositionSynchronizer.Sync(
_shadows,
entity.Id,
worldPosition,
orientation,
canonicalCellId,
liveCenterX,
liveCenterY);
}
else if (HasVisibleCell(record))
{
body.InWorld = true;
body.LastUpdateTime = currentTime;
_shadows.Suspend(entity.Id);
}
else
{
SuspendOutsideWorld(runtime, entity.Id);
}
_rootPoses?.UpdateRoot(entity);
return true;
if (!ExternalOwnerValid())
return false;
entity.SetPosition(snapshot.Position);
entity.Rotation = snapshot.Orientation;
entity.ParentCellId = snapshot.FullCellId;
_rootPoses?.UpdateRoot(entity);
return ExternalOwnerValid();
},
ExternalOwnerValid);
}
/// <summary>
@ -660,7 +594,7 @@ internal sealed class ProjectileController
internal bool TryGetBody(uint serverGuid, out PhysicsBody body)
{
if (TryGetCurrent(serverGuid, out _, out Runtime runtime))
if (TryGetCurrent(serverGuid, out _, out RuntimeProjectile runtime))
{
body = runtime.Body;
return true;
@ -672,8 +606,7 @@ internal sealed class ProjectileController
internal bool LeaveWorld(LiveEntityRecord record)
{
ArgumentNullException.ThrowIfNull(record);
if (record.ProjectileRuntime is not Runtime runtime
|| runtime.Generation != record.Generation
if (record.ProjectileRuntime is not RuntimeProjectile runtime
|| record.WorldEntity is not { } entity)
return false;
SuspendOutsideWorld(runtime, entity.Id);
@ -712,8 +645,7 @@ internal sealed class ProjectileController
_liveEntities.CopySpatialProjectileRecordsTo(_spatialProjectileSnapshot);
foreach (LiveEntityRecord record in _spatialProjectileSnapshot)
{
if (record.ProjectileRuntime is not Runtime runtime
|| runtime.Generation != record.Generation
if (record.ProjectileRuntime is not RuntimeProjectile runtime
|| !_liveEntities.IsCurrentSpatialProjectile(record, runtime)
|| record.WorldEntity is not { } entity)
continue;
@ -847,30 +779,34 @@ internal sealed class ProjectileController
{
ArgumentNullException.ThrowIfNull(record);
step = default;
if (!TryGetCurrent(record.ServerGuid, out LiveEntityRecord current, out Runtime runtime)
if (!TryGetCurrent(
record.ServerGuid,
out LiveEntityRecord current,
out RuntimeProjectile runtime)
|| !ReferenceEquals(record, current)
|| current.WorldEntity is not { } entity
|| IsHidden(current)
|| !HasVisibleCell(current))
return false;
runtime.Body.State = current.FinalPhysicsState;
bool isParented = current.Snapshot.ParentGuid is not null
|| current.Snapshot.Physics?.Parent is not null;
ProjectileQuantumPreparation preparation = _stepper.BeginQuantum(
runtime.Body,
quantum,
current.FullCellId,
runtime.CollisionSphere,
isParented);
if (!preparation.Simulated)
bool ExternalOwnerValid() =>
_liveEntities.IsCurrentRecord(current)
&& ReferenceEquals(current.WorldEntity, entity)
&& ReferenceEquals(current.ProjectileRuntime, runtime);
if (!_runtimeUpdater.TryBegin(
current.Canonical,
quantum,
current.ObjectClockEpoch,
ExternalOwnerValid,
out RuntimeProjectilePhysicsCommit runtimeCommit))
{
return false;
}
step = new QuantumStep(
current,
runtime,
entity,
preparation,
runtime.PredictionAuthorityVersion);
runtimeCommit);
return true;
}
@ -880,89 +816,42 @@ internal sealed class ProjectileController
int liveCenterY)
{
LiveEntityRecord current = step.Record;
Runtime runtime = step.Runtime;
WorldEntity entity = step.Entity;
if (!IsCurrentQuantumStep(step))
return false;
ProjectileAdvanceResult result = _stepper.CompleteQuantum(
runtime.Body,
step.Preparation,
runtime.CollisionSphere,
entity.Id,
designatedTargetId: 0u);
if (!result.Simulated || !IsCurrentQuantumStep(step))
return false;
uint priorCellId = current.FullCellId;
uint resolvedCellId = result.CellId != 0 ? result.CellId : priorCellId;
// Commit the retained CPhysicsObj's complete frame before Rebucket
// publishes a loaded-to-pending edge. Pending is still the same live
// object; its canonical body must already name the destination cell
// so later hydration cannot resurrect the source-cell frame.
runtime.Body.SnapToCell(
resolvedCellId,
runtime.Body.Position,
CellLocalFromWorld(
runtime.Body.Position,
resolvedCellId,
liveCenterX,
liveCenterY));
entity.SetPosition(runtime.Body.Position);
entity.Rotation = runtime.Body.Orientation;
entity.ParentCellId = resolvedCellId;
if (resolvedCellId != priorCellId
&& !_liveEntities.RebucketLiveEntity(current.ServerGuid, resolvedCellId))
{
return false;
}
// A legitimate destination can now be pending and therefore absent
// from the spatial-projectile index. Revalidate identity/mutation,
// not visibility, after the canonical cell commit.
if (!IsCurrentQuantumIdentity(step))
{
return false;
}
QuantumStep exactStep = step;
if (HasVisibleCell(current))
{
ShadowPositionSynchronizer.Sync(
_shadows,
entity.Id,
runtime.Body.Position,
runtime.Body.Orientation,
current.FullCellId,
liveCenterX,
liveCenterY);
_rootPoses?.UpdateRoot(entity);
if (!IsCurrentQuantumIdentity(step))
return false;
}
else
{
SuspendOutsideWorld(runtime, entity.Id);
}
return IsCurrentQuantumIdentity(step);
return _runtimeUpdater.Complete(
exactStep.RuntimeCommit,
liveCenterX,
liveCenterY,
snapshot =>
{
if (!IsCurrentQuantumIdentity(exactStep))
return false;
entity.SetPosition(snapshot.Position);
entity.Rotation = snapshot.Orientation;
entity.ParentCellId = snapshot.FullCellId;
if (HasVisibleCell(current))
_rootPoses?.UpdateRoot(entity);
return IsCurrentQuantumIdentity(exactStep);
});
}
private bool IsCurrentQuantumStep(in QuantumStep step) =>
IsCurrentQuantumIdentity(step)
&& _liveEntities.IsCurrentSpatialProjectile(step.Record, step.Runtime);
private bool IsCurrentQuantumIdentity(in QuantumStep step) =>
TryGetCurrent(
step.Record.ServerGuid,
out LiveEntityRecord current,
out Runtime runtime)
out RuntimeProjectile runtime)
&& ReferenceEquals(current, step.Record)
&& ReferenceEquals(runtime, step.Runtime)
&& ReferenceEquals(current.WorldEntity, step.Entity)
&& ReferenceEquals(runtime, step.RuntimeCommit.Projectile)
&& runtime.PredictionAuthorityVersion
== step.PredictionAuthorityVersion;
== step.RuntimeCommit.PredictionAuthorityVersion;
private void OnProjectionVisibilityChanged(LiveEntityRecord record, bool visible)
{
if (record.ProjectileRuntime is not Runtime runtime
if (record.ProjectileRuntime is not RuntimeProjectile runtime
|| record.WorldEntity is not { } entity
|| !_liveEntities.TryGetRecord(record.ServerGuid, out LiveEntityRecord current)
|| !ReferenceEquals(current, record)
@ -1020,22 +909,13 @@ internal sealed class ProjectileController
private static bool IsHidden(LiveEntityRecord record) =>
(record.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0;
private void SuspendOutsideWorld(Runtime runtime, uint localEntityId)
private void SuspendOutsideWorld(RuntimeProjectile runtime, uint localEntityId)
{
runtime.Body.InWorld = false;
Deactivate(runtime.Body);
_shadows.Suspend(localEntityId);
}
private static void Activate(PhysicsBody body, double currentTime)
{
if ((body.State & PhysicsStateFlags.Static) != 0)
return;
if ((body.TransientState & TransientStateFlags.Active) == 0)
body.LastUpdateTime = currentTime;
body.TransientState |= TransientStateFlags.Active;
}
private static void Deactivate(PhysicsBody body) =>
body.TransientState &= ~TransientStateFlags.Active;
@ -1089,11 +969,10 @@ internal sealed class ProjectileController
private bool TryGetCurrent(
uint serverGuid,
out LiveEntityRecord record,
out Runtime runtime)
out RuntimeProjectile runtime)
{
if (_liveEntities.TryGetRecord(serverGuid, out record!)
&& record.ProjectileRuntime is Runtime found
&& found.Generation == record.Generation)
&& record.ProjectileRuntime is RuntimeProjectile found)
{
runtime = found;
return true;

View file

@ -83,8 +83,8 @@ internal sealed class LiveEntityAnimationScheduler
LiveEntityAnimationState? animation = record.AnimationRuntime as LiveEntityAnimationState;
RemoteMotion? remote = record.RemoteMotionRuntime as RemoteMotion;
ProjectileController.Runtime? projectile =
record.ProjectileRuntime as ProjectileController.Runtime;
RuntimeProjectile? projectile =
record.ProjectileRuntime as RuntimeProjectile;
ulong objectClockEpoch = record.ObjectClockEpoch;
if (!IsCurrent(
@ -167,7 +167,7 @@ internal sealed class LiveEntityAnimationScheduler
WorldEntity entity,
LiveEntityAnimationState? animation,
RemoteMotion? remote,
ProjectileController.Runtime? projectile,
RuntimeProjectile? projectile,
float elapsedSeconds,
Vector3? playerPosition,
bool localHiddenPartPoseDirty,
@ -508,7 +508,7 @@ internal sealed class LiveEntityAnimationScheduler
WorldEntity entity,
LiveEntityAnimationState? animation,
RemoteMotion? remote,
ProjectileController.Runtime? projectile,
RuntimeProjectile? projectile,
ulong objectClockEpoch) =>
runtime.IsCurrentSpatialRootObject(record)
&& record.ObjectClockEpoch == objectClockEpoch

View file

@ -20,12 +20,6 @@ public interface ILiveEntityAnimationRuntime
uint CurrentMotion { get; }
}
/// <summary>Projectile physics state owned by a live object.</summary>
public interface ILiveEntityProjectileRuntime
{
PhysicsBody Body { get; }
}
/// <summary>Marker for the DAT-driven effect profile added by the effect phase.</summary>
public interface ILiveEntityEffectProfile { }
@ -257,7 +251,7 @@ public sealed class LiveEntityRecord
get => Canonical.RequiresRemotePlacementRuntime;
set => _directory.SetRequiresRemotePlacementRuntime(Canonical, value);
}
public ILiveEntityProjectileRuntime? ProjectileRuntime { get; internal set; }
public IRuntimeProjectile? ProjectileRuntime => Canonical.Projectile;
public ILiveEntityEffectProfile? EffectProfile { get; internal set; }
public bool ResourcesRegistered { get; internal set; }
public bool IsSpatiallyProjected { get; internal set; }
@ -396,7 +390,6 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
// projection. Hidden and Frozen remain members: their state controls what
// each retail update path advances after the O(active) lookup.
private readonly Dictionary<RuntimeEntityKey, ILiveEntityAnimationRuntime> _spatialAnimations = new();
private readonly Dictionary<RuntimeEntityKey, ILiveEntityProjectileRuntime> _spatialProjectiles = new();
private readonly List<RuntimeEntityRecord> _spatialRootCanonicalScratch = new();
private readonly List<RuntimeEntityRecord> _spatialRemoteCanonicalScratch = new();
private bool _isClearing;
@ -477,7 +470,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
}
internal int SpatialAnimationRuntimeCount => _spatialAnimations.Count;
internal int SpatialRemoteMotionRuntimeCount => _physics.SpatialRemoteCount;
internal int SpatialProjectileRuntimeCount => _spatialProjectiles.Count;
internal int SpatialProjectileRuntimeCount => _physics.SpatialProjectileCount;
internal int SpatialRootObjectCount => _physics.SpatialRootCount;
public ParentAttachmentState ParentAttachments => _directory.ParentAttachments;
@ -1475,9 +1468,13 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
return true;
}
public void SetProjectileRuntime(uint serverGuid, ILiveEntityProjectileRuntime runtime)
internal IRuntimeProjectile BindProjectileRuntime(
uint serverGuid,
PhysicsBody body,
ProjectileCollisionSphere collisionSphere,
Func<bool>? externalOwnerValid = null)
{
ArgumentNullException.ThrowIfNull(runtime);
ArgumentNullException.ThrowIfNull(body);
if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|| record.WorldEntity is null)
{
@ -1487,24 +1484,28 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
if (record.PhysicsBodyAcquisitionInProgress && record.PhysicsBody is null)
throw new InvalidOperationException(
$"Live entity 0x{serverGuid:X8} cannot bind projectile physics during physics-body acquisition.");
PhysicsBody candidateBody = runtime.Body;
if (record.PhysicsBody is { } canonicalBody
&& !ReferenceEquals(canonicalBody, candidateBody))
&& !ReferenceEquals(canonicalBody, body))
{
throw new InvalidOperationException(
$"Live entity 0x{serverGuid:X8} cannot replace its canonical physics body within one incarnation.");
}
record.ProjectileRuntime = runtime;
record.PhysicsBody = candidateBody;
candidateBody.State = record.FinalPhysicsState;
SynchronizePhysicsBodyActiveState(record);
IRuntimeProjectile runtime = _physics.BindProjectile(
record.Canonical,
body,
collisionSphere,
() => IsCurrentRecord(record)
&& record.WorldEntity is { } entity
&& record.LocalEntityId == entity.Id
&& (externalOwnerValid?.Invoke() ?? true));
RefreshSpatialRuntimeIndexes(record);
return runtime;
}
public bool TryGetProjectileRuntime(
uint serverGuid,
out ILiveEntityProjectileRuntime runtime)
out IRuntimeProjectile runtime)
{
if (_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
&& record.ProjectileRuntime is { } found)
@ -1523,7 +1524,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|| record.ProjectileRuntime is null)
return false;
record.ProjectileRuntime = null;
_physics.ClearProjectile(record.Canonical);
RefreshSpatialRuntimeIndexes(record);
return true;
}
@ -2021,11 +2022,11 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
ArgumentNullException.ThrowIfNull(destination);
destination.Clear();
foreach ((RuntimeEntityKey key, ILiveEntityProjectileRuntime runtime)
in _spatialProjectiles)
_physics.CopySpatialProjectilesTo(_spatialRootCanonicalScratch);
for (int i = 0; i < _spatialRootCanonicalScratch.Count; i++)
{
if (_projections.TryGet(key, out LiveEntityRecord? record)
&& ReferenceEquals(record.ProjectileRuntime, runtime)
RuntimeEntityRecord canonical = _spatialRootCanonicalScratch[i];
if (_projections.TryGet(canonical, out LiveEntityRecord? record)
&& HasSpatialRuntimeProjection(record))
{
destination.Add(record);
@ -2035,12 +2036,9 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
internal bool IsCurrentSpatialProjectile(
LiveEntityRecord record,
ILiveEntityProjectileRuntime runtime) =>
IRuntimeProjectile runtime) =>
IsCurrentRecord(record)
&& ReferenceEquals(record.ProjectileRuntime, runtime)
&& record.ProjectionKey is { } key
&& _spatialProjectiles.TryGetValue(key, out var indexed)
&& ReferenceEquals(indexed, runtime)
&& _physics.IsSpatialProjectile(record.Canonical, runtime)
&& HasSpatialRuntimeProjection(record);
public bool IsHidden(uint serverGuid) =>
@ -2459,17 +2457,6 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
}
}
if (spatial && record.ProjectileRuntime is { } projectile)
{
_spatialProjectiles[key] = projectile;
}
else if (current
|| (record.ProjectileRuntime is { } retainedProjectile
&& _spatialProjectiles.TryGetValue(key, out var indexedProjectile)
&& ReferenceEquals(indexedProjectile, retainedProjectile)))
{
_spatialProjectiles.Remove(key);
}
}
private void RemoveSpatialRuntimeIndexes(LiveEntityRecord record)
@ -2486,11 +2473,6 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
_spatialAnimations.Remove(key);
}
if (_spatialProjectiles.TryGetValue(key, out var indexedProjectile)
&& ReferenceEquals(indexedProjectile, record.ProjectileRuntime))
{
_spatialProjectiles.Remove(key);
}
}
private void OnSpatialVisibilityChanged(RuntimeEntityKey key, bool visible)
@ -2681,7 +2663,6 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
}
_spatialAnimations.Clear();
_spatialProjectiles.Clear();
_physics.ClearSpatialWorksets();
_projections.ClearConverged();
_spatial.ClearLiveEntityLifetimeState();
@ -2717,6 +2698,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
// a retryable tombstone until every external cleanup acknowledges
// success.
RemoveSpatialRuntimeIndexes(record);
_physics.ClearProjectile(record.Canonical);
if (!record.RuntimeComponentsTeardownCompleted)
{
@ -2756,7 +2738,6 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
record.PhysicsHost = null;
record.RequiresRemotePlacementRuntime = false;
record.PhysicsBody = null;
record.ProjectileRuntime = null;
record.EffectProfile = null;
record.IsSpatiallyProjected = false;
record.IsSpatiallyVisible = false;

View file

@ -355,6 +355,22 @@ public sealed class RuntimeEntityDirectory
record.RemoteMotionBindingInProgress = value;
}
public void SetProjectile(
RuntimeEntityRecord record,
IRuntimeProjectile? projectile)
{
EnsureKnown(record);
record.Projectile = projectile;
}
public void SetProjectileBindingInProgress(
RuntimeEntityRecord record,
bool value)
{
EnsureKnown(record);
record.ProjectileBindingInProgress = value;
}
public void SetRequiresRemotePlacementRuntime(
RuntimeEntityRecord record,
bool value)

View file

@ -97,6 +97,20 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
Events = new RuntimeEntityObjectEventStream(Entities, Objects);
}
internal RuntimeEntityObjectLifetime(
PhysicsEngine physicsEngine,
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId)
{
ArgumentNullException.ThrowIfNull(physicsEngine);
Entities = new RuntimeEntityDirectory(firstLocalEntityId);
Physics = new RuntimePhysicsState(Entities, physicsEngine);
Objects = new ClientObjectTable();
var views = new RuntimeEntityObjectViews(Entities, Objects);
EntityView = views.Entities;
InventoryView = views.Inventory;
Events = new RuntimeEntityObjectEventStream(Entities, Objects);
}
public RuntimeEntityDirectory Entities { get; }
public RuntimePhysicsState Physics { get; }
public ClientObjectTable Objects { get; }
@ -344,6 +358,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
Physics.RemoveSpatialProjection(canonical);
Entities.SetRemoteMotion(canonical, null);
Entities.SetRemoteMotionBindingInProgress(canonical, false);
Entities.SetProjectile(canonical, null);
Entities.SetProjectileBindingInProgress(canonical, false);
Entities.SetRequiresRemotePlacementRuntime(canonical, false);
Entities.SetPhysicsHost(canonical, null);
Entities.SetPhysicsBody(canonical, null);

View file

@ -71,6 +71,8 @@ public sealed class RuntimeEntityRecord
public bool PhysicsBodyAcquisitionInProgress { get; internal set; }
public IRuntimeRemoteMotion? RemoteMotion { get; internal set; }
public bool RemoteMotionBindingInProgress { get; internal set; }
public IRuntimeProjectile? Projectile { get; internal set; }
public bool ProjectileBindingInProgress { get; internal set; }
public bool RequiresRemotePlacementRuntime { get; internal set; }
public AcDream.Core.Physics.Motion.IPhysicsObjHost? PhysicsHost { get; internal set; }
public ulong PositionAuthorityVersion { get; private set; }

View file

@ -8,6 +8,7 @@ public readonly record struct RuntimePhysicsOwnershipSnapshot(
int RetainedShadowRegistrationCount,
int SpatialRootCount,
int SpatialRemoteCount,
int SpatialProjectileCount,
int CollisionAdmissionCount,
int CollisionGenerationCount,
bool OwnsProductionDataCache,
@ -64,6 +65,8 @@ public sealed class RuntimePhysicsState : IDisposable
_spatialRoots = new();
private readonly Dictionary<RuntimeEntityKey, IRuntimeRemoteMotion>
_spatialRemotes = new();
private readonly Dictionary<RuntimeEntityKey, IRuntimeProjectile>
_spatialProjectiles = new();
private readonly Dictionary<uint, ulong> _collisionGenerations = new();
private readonly Dictionary<uint, RuntimeCollisionAdmission>
_collisionAdmissions = new();
@ -83,11 +86,22 @@ public sealed class RuntimePhysicsState : IDisposable
};
}
internal RuntimePhysicsState(
RuntimeEntityDirectory entities,
PhysicsEngine engine)
{
Entities = entities ?? throw new ArgumentNullException(nameof(entities));
Engine = engine ?? throw new ArgumentNullException(nameof(engine));
DataCache = engine.DataCache ?? PhysicsDataCache.CreateProduction();
Engine.DataCache = DataCache;
}
internal RuntimeEntityDirectory Entities { get; }
public PhysicsEngine Engine { get; }
public PhysicsDataCache DataCache { get; }
public int SpatialRootCount => _spatialRoots.Count;
public int SpatialRemoteCount => _spatialRemotes.Count;
public int SpatialProjectileCount => _spatialProjectiles.Count;
public RuntimePhysicsOwnershipSnapshot CaptureOwnership() =>
new(
@ -95,6 +109,7 @@ public sealed class RuntimePhysicsState : IDisposable
Engine.ShadowObjects.RetainedRegistrationCount,
_spatialRoots.Count,
_spatialRemotes.Count,
_spatialProjectiles.Count,
_collisionAdmissions.Count,
_collisionGenerations.Count,
ReferenceEquals(Engine.DataCache, DataCache),
@ -123,6 +138,10 @@ public sealed class RuntimePhysicsState : IDisposable
_spatialRemotes[key] = remote;
else
_spatialRemotes.Remove(key);
if (record.Projectile is { } projectile)
_spatialProjectiles[key] = projectile;
else
_spatialProjectiles.Remove(key);
return;
}
@ -148,6 +167,167 @@ public sealed class RuntimePhysicsState : IDisposable
_spatialRemotes.Remove(key);
}
public void RefreshProjectileComponent(RuntimeEntityRecord record)
{
EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(record);
if (record.Key is not { } key
|| !_spatialRoots.TryGetValue(key, out RuntimeEntityRecord? root)
|| !ReferenceEquals(root, record)
|| !Entities.IsCurrent(record))
{
RemoveSpatialProjectile(record);
return;
}
if (record.Projectile is { } projectile)
_spatialProjectiles[key] = projectile;
else
_spatialProjectiles.Remove(key);
}
/// <summary>
/// Installs the one projectile component for an exact canonical
/// incarnation. App may prepare the DAT collision sphere, but Runtime owns
/// component identity, prediction invalidation, and workset membership.
/// </summary>
public IRuntimeProjectile BindProjectile(
RuntimeEntityRecord record,
PhysicsBody body,
ProjectileCollisionSphere collisionSphere,
Func<bool>? externalOwnerValid = null)
{
EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(record);
ArgumentNullException.ThrowIfNull(body);
EnsureCurrent(record);
if (!collisionSphere.IsValid)
{
throw new ArgumentOutOfRangeException(
nameof(collisionSphere),
"A Runtime projectile requires one valid prepared Setup sphere.");
}
if (!(externalOwnerValid?.Invoke() ?? true))
{
throw new InvalidOperationException(
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} changed external ownership before projectile binding.");
}
if (record.Projectile is { } retained)
{
if (!ReferenceEquals(retained.Body, body)
|| !ReferenceEquals(record.PhysicsBody, body))
{
throw new InvalidOperationException(
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} projectile changed its canonical physics body.");
}
retained.Body.State = record.FinalPhysicsState;
RefreshProjectileComponent(record);
return retained;
}
if (record.ProjectileBindingInProgress)
{
throw new InvalidOperationException(
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} projectile binding is already in progress.");
}
if (!ReferenceEquals(record.PhysicsBody, body))
{
throw new InvalidOperationException(
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} projectile must borrow its canonical physics body.");
}
ulong sessionVersion = Entities.SessionLifetimeVersion;
Entities.SetProjectileBindingInProgress(record, true);
try
{
var component = new RuntimeProjectile(body, collisionSphere);
if (Entities.SessionLifetimeVersion != sessionVersion
|| !Entities.IsCurrent(record)
|| !ReferenceEquals(record.PhysicsBody, body)
|| record.Projectile is not null
|| !(externalOwnerValid?.Invoke() ?? true))
{
throw new InvalidOperationException(
$"Runtime entity 0x{record.ServerGuid:X8}/{record.Incarnation} changed ownership during projectile binding.");
}
Entities.SetProjectile(record, component);
body.State = record.FinalPhysicsState;
SynchronizeBodyActiveState(record);
RefreshProjectileComponent(record);
return component;
}
finally
{
if (Entities.IsCurrent(record))
Entities.SetProjectileBindingInProgress(record, false);
else
record.ProjectileBindingInProgress = false;
}
}
public bool ClearProjectile(RuntimeEntityRecord record)
{
EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(record);
if (record.Projectile is null)
return false;
Entities.SetProjectile(record, null);
Entities.SetProjectileBindingInProgress(record, false);
RefreshProjectileComponent(record);
return true;
}
/// <summary>
/// Commits retail <c>CPhysicsObj::set_velocity</c> (0x005113F0) and,
/// when supplied, the paired <c>set_omega</c> write to the incarnation's
/// canonical body. Runtime owns the shared body/object-clock activation
/// edge; graphical hosts only validate and translate inbound payloads.
/// </summary>
internal bool TryCommitAuthoritativeVector(
RuntimeEntityRecord record,
PhysicsBody body,
System.Numerics.Vector3 velocity,
System.Numerics.Vector3? angularVelocity,
double currentTime,
Func<bool>? externalOwnerValid = null)
{
EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(record);
ArgumentNullException.ThrowIfNull(body);
if (!IsFinite(velocity)
|| (angularVelocity is { } omega && !IsFinite(omega))
|| !double.IsFinite(currentTime)
|| !Entities.IsCurrent(record)
|| !ReferenceEquals(record.PhysicsBody, body)
|| !(externalOwnerValid?.Invoke() ?? true))
{
return false;
}
bool wasBodyActive =
(body.TransientState & TransientStateFlags.Active) != 0;
body.set_velocity(velocity);
if ((record.FinalPhysicsState & PhysicsStateFlags.Static) != 0)
{
if (!wasBodyActive)
body.TransientState &= ~TransientStateFlags.Active;
}
else
{
bool clockReactivated = record.ObjectClock.Activate();
if (clockReactivated || !wasBodyActive)
body.LastUpdateTime = currentTime;
}
if (angularVelocity is { } acceptedOmega)
body.Omega = acceptedOmega;
return Entities.IsCurrent(record)
&& ReferenceEquals(record.PhysicsBody, body)
&& (externalOwnerValid?.Invoke() ?? true);
}
public void SetRemoteMotion(
RuntimeEntityRecord record,
IRuntimeRemoteMotion runtime,
@ -476,6 +656,7 @@ public sealed class RuntimePhysicsState : IDisposable
_spatialRoots.Remove(key);
}
RemoveSpatialRemote(record);
RemoveSpatialProjectile(record);
}
public bool IsSpatialRoot(RuntimeEntityRecord record) =>
@ -493,6 +674,15 @@ public sealed class RuntimePhysicsState : IDisposable
&& _spatialRemotes.TryGetValue(key, out IRuntimeRemoteMotion? indexed)
&& ReferenceEquals(indexed, remote);
public bool IsSpatialProjectile(
RuntimeEntityRecord record,
IRuntimeProjectile projectile) =>
IsSpatialRoot(record)
&& ReferenceEquals(record.Projectile, projectile)
&& record.Key is { } key
&& _spatialProjectiles.TryGetValue(key, out IRuntimeProjectile? indexed)
&& ReferenceEquals(indexed, projectile);
public void CopySpatialRootsTo(List<RuntimeEntityRecord> destination)
{
EnsureNotDisposed();
@ -529,6 +719,26 @@ public sealed class RuntimePhysicsState : IDisposable
}
}
public void CopySpatialProjectilesTo(List<RuntimeEntityRecord> destination)
{
EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(destination);
destination.Clear();
foreach ((RuntimeEntityKey key, IRuntimeProjectile projectile)
in _spatialProjectiles)
{
if (Entities.TryGetByLocalId(
key.LocalEntityId,
out RuntimeEntityRecord record)
&& record.Key == key
&& ReferenceEquals(record.Projectile, projectile)
&& IsSpatialRoot(record))
{
destination.Add(record);
}
}
}
internal bool CommitOrdinaryCell(
RuntimeEntityRecord record,
PhysicsBody body,
@ -551,10 +761,34 @@ public sealed class RuntimePhysicsState : IDisposable
IsExactOwner);
}
internal bool CommitProjectileCell(
RuntimeEntityRecord record,
IRuntimeProjectile projectile,
ulong predictionAuthorityVersion,
uint fullCellId,
Func<bool>? externalOwnerValid)
{
EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(record);
ArgumentNullException.ThrowIfNull(projectile);
bool IsExactOwner() =>
Entities.IsCurrent(record)
&& ReferenceEquals(record.Projectile, projectile)
&& ReferenceEquals(record.PhysicsBody, projectile.Body)
&& projectile.PredictionAuthorityVersion
== predictionAuthorityVersion
&& (externalOwnerValid?.Invoke() ?? true);
return CommitCanonicalCell(
record,
fullCellId,
IsExactOwner);
}
public void ClearSpatialWorksets()
{
EnsureNotDisposed();
_spatialRemotes.Clear();
_spatialProjectiles.Clear();
_spatialRoots.Clear();
}
@ -672,6 +906,7 @@ public sealed class RuntimePhysicsState : IDisposable
if (_disposed)
return;
_spatialRemotes.Clear();
_spatialProjectiles.Clear();
_spatialRoots.Clear();
_collisionAdmissions.Clear();
_collisionGenerations.Clear();
@ -730,6 +965,21 @@ public sealed class RuntimePhysicsState : IDisposable
}
}
private void RemoveSpatialProjectile(RuntimeEntityRecord record)
{
if (record.Key is not { } key)
return;
if (_spatialProjectiles.TryGetValue(
key,
out IRuntimeProjectile? projectile)
&& (record.Projectile is null
|| ReferenceEquals(projectile, record.Projectile)
|| !Entities.IsCurrent(record)))
{
_spatialProjectiles.Remove(key);
}
}
private void EnsureNotDisposed() =>
ObjectDisposedException.ThrowIf(_disposed, this);
@ -775,6 +1025,11 @@ public sealed class RuntimePhysicsState : IDisposable
private static uint CanonicalLandblock(uint value) =>
(value & 0xFFFF0000u) | 0xFFFFu;
private static bool IsFinite(System.Numerics.Vector3 value) =>
float.IsFinite(value.X)
&& float.IsFinite(value.Y)
&& float.IsFinite(value.Z);
private static void SynchronizeBodyActiveState(RuntimeEntityRecord record)
{
if (record.PhysicsBody is not { } body)

View file

@ -0,0 +1,40 @@
using AcDream.Core.Physics;
namespace AcDream.Runtime.Physics;
/// <summary>
/// Presentation-free projectile component attached to one canonical Runtime
/// entity incarnation. The body is retail's one <c>CPhysicsObj</c>; the
/// prepared Setup sphere and prediction version die with the same incarnation.
/// </summary>
public interface IRuntimeProjectile
{
PhysicsBody Body { get; }
ProjectileCollisionSphere CollisionSphere { get; }
ulong PredictionAuthorityVersion { get; }
}
internal sealed class RuntimeProjectile : IRuntimeProjectile
{
internal RuntimeProjectile(
PhysicsBody body,
ProjectileCollisionSphere collisionSphere)
{
Body = body ?? throw new ArgumentNullException(nameof(body));
if (!collisionSphere.IsValid)
{
throw new ArgumentOutOfRangeException(
nameof(collisionSphere),
"A Runtime projectile requires one valid prepared Setup sphere.");
}
CollisionSphere = collisionSphere;
}
public PhysicsBody Body { get; }
public ProjectileCollisionSphere CollisionSphere { get; }
public ulong PredictionAuthorityVersion { get; private set; }
internal void InvalidatePrediction() =>
PredictionAuthorityVersion++;
}

View file

@ -0,0 +1,500 @@
using System.Numerics;
using AcDream.Core.Physics;
using AcDream.Runtime.Entities;
namespace AcDream.Runtime.Physics;
internal sealed class RuntimeProjectilePhysicsCommit
{
internal required RuntimeProjectilePhysicsUpdater Owner { get; init; }
internal required RuntimeEntityRecord Record { get; init; }
internal required RuntimeProjectile Projectile { get; init; }
internal required ProjectileQuantumPreparation Preparation { get; init; }
internal required ulong PredictionAuthorityVersion { get; init; }
internal required ulong ObjectClockEpoch { get; init; }
internal required Func<bool>? ExternalOwnerValid { get; init; }
internal bool Completed { get; set; }
}
/// <summary>
/// Presentation-free owner of the projectile slice in retail
/// <c>CPhysicsObj::UpdateObjectInternal</c> (<c>0x005156B0</c>). The caller
/// preserves retail hook ordering by holding the returned transaction across
/// <c>process_hooks</c>; Runtime owns integration, transition, prediction
/// versions, canonical cell state, and the physics shadow.
/// </summary>
internal sealed class RuntimeProjectilePhysicsUpdater
{
private readonly RuntimePhysicsState _physics;
private readonly ProjectilePhysicsStepper _stepper;
internal RuntimeProjectilePhysicsUpdater(RuntimePhysicsState physics)
{
_physics = physics ?? throw new ArgumentNullException(nameof(physics));
_stepper = new ProjectilePhysicsStepper(physics.Engine);
}
internal bool TryBegin(
RuntimeEntityRecord record,
float quantum,
ulong objectClockEpoch,
Func<bool>? externalOwnerValid,
out RuntimeProjectilePhysicsCommit commit)
{
ArgumentNullException.ThrowIfNull(record);
if (record.Projectile is not RuntimeProjectile projectile
|| !IsSpatialCurrent(
record,
projectile,
projectile.PredictionAuthorityVersion,
objectClockEpoch,
externalOwnerValid)
|| (record.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0
|| record.FullCellId == 0)
{
commit = null!;
return false;
}
projectile.Body.State = record.FinalPhysicsState;
bool isParented = record.Snapshot.ParentGuid is not null
|| record.Snapshot.Physics?.Parent is not null;
ProjectileQuantumPreparation preparation = _stepper.BeginQuantum(
projectile.Body,
quantum,
record.FullCellId,
projectile.CollisionSphere,
isParented);
if (!preparation.Simulated
|| !IsSpatialCurrent(
record,
projectile,
projectile.PredictionAuthorityVersion,
objectClockEpoch,
externalOwnerValid))
{
commit = null!;
return false;
}
commit = new RuntimeProjectilePhysicsCommit
{
Owner = this,
Record = record,
Projectile = projectile,
Preparation = preparation,
PredictionAuthorityVersion =
projectile.PredictionAuthorityVersion,
ObjectClockEpoch = objectClockEpoch,
ExternalOwnerValid = externalOwnerValid,
};
return true;
}
internal bool Complete(
RuntimeProjectilePhysicsCommit commit,
int liveCenterX,
int liveCenterY,
Func<RuntimePhysicsFrameSnapshot, bool> acknowledgeProjection)
{
ArgumentNullException.ThrowIfNull(commit);
ArgumentNullException.ThrowIfNull(acknowledgeProjection);
if (!ReferenceEquals(commit.Owner, this))
{
throw new InvalidOperationException(
"A projectile-physics commit belongs to another Runtime owner.");
}
if (commit.Completed)
{
throw new InvalidOperationException(
"A projectile-physics commit has already completed.");
}
commit.Completed = true;
if (!IsSpatialCurrent(
commit.Record,
commit.Projectile,
commit.PredictionAuthorityVersion,
commit.ObjectClockEpoch,
commit.ExternalOwnerValid))
{
return false;
}
PhysicsBody body = commit.Projectile.Body;
ProjectileAdvanceResult result = _stepper.CompleteQuantum(
body,
commit.Preparation,
commit.Projectile.CollisionSphere,
commit.Record.LocalEntityId ?? 0u,
designatedTargetId: 0u);
if (!result.Simulated
|| !IsSpatialCurrent(
commit.Record,
commit.Projectile,
commit.PredictionAuthorityVersion,
commit.ObjectClockEpoch,
commit.ExternalOwnerValid))
{
return false;
}
uint resolvedCellId = result.CellId != 0
? result.CellId
: commit.Record.FullCellId;
body.SnapToCell(
resolvedCellId,
body.Position,
CellLocalFromWorld(
body.Position,
resolvedCellId,
liveCenterX,
liveCenterY));
var snapshot = new RuntimePhysicsFrameSnapshot(
body.Position,
body.Orientation,
resolvedCellId);
if (!_physics.CommitProjectileCell(
commit.Record,
commit.Projectile,
commit.PredictionAuthorityVersion,
resolvedCellId,
commit.ExternalOwnerValid)
|| !IsIdentityCurrent(
commit.Record,
commit.Projectile,
commit.PredictionAuthorityVersion,
commit.ExternalOwnerValid)
|| !acknowledgeProjection(snapshot)
|| !IsIdentityCurrent(
commit.Record,
commit.Projectile,
commit.PredictionAuthorityVersion,
commit.ExternalOwnerValid))
{
return false;
}
if (_physics.IsSpatialProjectile(
commit.Record,
commit.Projectile)
&& (commit.Record.FinalPhysicsState
& PhysicsStateFlags.Hidden) == 0)
{
ShadowPositionSynchronizer.Sync(
_physics.Engine.ShadowObjects,
commit.Record.LocalEntityId ?? 0u,
body.Position,
body.Orientation,
commit.Record.FullCellId,
liveCenterX,
liveCenterY);
}
else
{
_physics.Engine.ShadowObjects.Suspend(
commit.Record.LocalEntityId ?? 0u);
}
return IsIdentityCurrent(
commit.Record,
commit.Projectile,
commit.PredictionAuthorityVersion,
commit.ExternalOwnerValid);
}
internal bool ApplyAuthoritativeVector(
RuntimeEntityRecord record,
ulong expectedVectorAuthorityVersion,
ulong expectedVelocityAuthorityVersion,
Vector3 velocity,
Vector3 angularVelocity,
double currentTime,
Func<bool>? externalOwnerValid = null)
{
ArgumentNullException.ThrowIfNull(record);
if (!TryGetCurrent(
record,
externalOwnerValid,
out RuntimeProjectile projectile)
|| record.VectorAuthorityVersion
!= expectedVectorAuthorityVersion
|| record.VelocityAuthorityVersion
!= expectedVelocityAuthorityVersion)
{
return false;
}
if ((record.FinalPhysicsState & PhysicsStateFlags.Missile) == 0
&& record.RemoteMotion is not null)
{
return false;
}
if (!IsFinite(velocity)
|| !IsFinite(angularVelocity)
|| !double.IsFinite(currentTime))
{
return true;
}
projectile.InvalidatePrediction();
_ = _physics.TryCommitAuthoritativeVector(
record,
projectile.Body,
velocity,
angularVelocity,
currentTime,
externalOwnerValid);
return true;
}
internal bool ApplyAuthoritativeState(
RuntimeEntityRecord record,
ulong expectedStateAuthorityVersion,
PhysicsStateFlags state,
double effectiveClock,
int liveCenterX,
int liveCenterY,
Func<bool>? externalOwnerValid = null)
{
ArgumentNullException.ThrowIfNull(record);
if (!TryGetCurrent(
record,
externalOwnerValid,
out RuntimeProjectile projectile)
|| record.StateAuthorityVersion
!= expectedStateAuthorityVersion)
{
return false;
}
projectile.InvalidatePrediction();
PhysicsBody body = projectile.Body;
if (!double.IsFinite(effectiveClock))
{
effectiveClock = double.IsFinite(body.LastUpdateTime)
? body.LastUpdateTime
: 0d;
}
bool wasMissile =
(body.State & PhysicsStateFlags.Missile) != 0;
body.State = state;
if ((state & PhysicsStateFlags.Missile) != 0 && !wasMissile)
{
body.LastUpdateTime = effectiveClock;
if (record.FullCellId != 0)
{
body.SnapToCell(
record.FullCellId,
body.Position,
CellLocalFromWorld(
body.Position,
record.FullCellId,
liveCenterX,
liveCenterY));
}
}
return true;
}
internal bool ApplyAuthoritativePosition(
RuntimeEntityRecord record,
ulong expectedPositionAuthorityVersion,
ulong expectedVelocityAuthorityVersion,
Vector3 worldPosition,
Vector3 cellLocalPosition,
Quaternion orientation,
Vector3 velocity,
uint fullCellId,
double currentTime,
int liveCenterX,
int liveCenterY,
Func<RuntimePhysicsFrameSnapshot, bool> acknowledgeProjection,
Func<bool>? externalOwnerValid = null)
{
ArgumentNullException.ThrowIfNull(record);
ArgumentNullException.ThrowIfNull(acknowledgeProjection);
if (!TryGetCurrent(
record,
externalOwnerValid,
out RuntimeProjectile projectile)
|| record.PositionAuthorityVersion
!= expectedPositionAuthorityVersion
|| (record.FinalPhysicsState
& PhysicsStateFlags.Missile) == 0)
{
return false;
}
if (!double.IsFinite(currentTime)
|| !IsFinite(worldPosition)
|| !IsFinite(cellLocalPosition)
|| !IsFinite(velocity)
|| !PositionFrameValidation.IsValid(
fullCellId,
cellLocalPosition,
orientation))
{
return true;
}
PhysicsBody body = projectile.Body;
projectile.InvalidatePrediction();
ulong predictionVersion = projectile.PredictionAuthorityVersion;
bool wasInWorld = body.InWorld;
body.Orientation = orientation;
body.SnapToCell(fullCellId, worldPosition, cellLocalPosition);
body.State = record.FinalPhysicsState;
if (record.VelocityAuthorityVersion
== expectedVelocityAuthorityVersion)
{
_ = _physics.TryCommitAuthoritativeVector(
record,
body,
velocity,
angularVelocity: null,
currentTime,
externalOwnerValid);
}
bool IsExactOwner() =>
TryGetCurrent(
record,
externalOwnerValid,
out RuntimeProjectile current)
&& ReferenceEquals(current, projectile)
&& current.PredictionAuthorityVersion == predictionVersion
&& record.PositionAuthorityVersion
== expectedPositionAuthorityVersion;
if (!_physics.CommitProjectileCell(
record,
projectile,
predictionVersion,
body.CellPosition.ObjCellId,
IsExactOwner)
|| !IsExactOwner())
{
// This packet was accepted for the old incarnation. A re-entrant
// observer displaced it, so the replacement owns another body.
return true;
}
var snapshot = new RuntimePhysicsFrameSnapshot(
body.Position,
body.Orientation,
body.CellPosition.ObjCellId);
if (!acknowledgeProjection(snapshot) || !IsExactOwner())
return true;
bool spatial = _physics.IsSpatialProjectile(record, projectile);
bool hidden =
(record.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0;
uint localId = record.LocalEntityId ?? 0u;
if (spatial && !hidden)
{
if (!wasInWorld)
{
body.LastUpdateTime = currentTime;
Activate(body, currentTime);
}
body.InWorld = true;
ShadowPositionSynchronizer.Sync(
_physics.Engine.ShadowObjects,
localId,
body.Position,
body.Orientation,
record.FullCellId,
liveCenterX,
liveCenterY);
}
else if (spatial)
{
body.InWorld = true;
body.LastUpdateTime = currentTime;
_physics.Engine.ShadowObjects.Suspend(localId);
}
else
{
body.InWorld = false;
Deactivate(body);
_physics.Engine.ShadowObjects.Suspend(localId);
}
return true;
}
private bool IsSpatialCurrent(
RuntimeEntityRecord record,
RuntimeProjectile projectile,
ulong predictionAuthorityVersion,
ulong objectClockEpoch,
Func<bool>? externalOwnerValid) =>
_physics.IsSpatialProjectile(record, projectile)
&& record.ObjectClockEpoch == objectClockEpoch
&& IsIdentityCurrent(
record,
projectile,
predictionAuthorityVersion,
externalOwnerValid);
private bool IsIdentityCurrent(
RuntimeEntityRecord record,
RuntimeProjectile projectile,
ulong predictionAuthorityVersion,
Func<bool>? externalOwnerValid) =>
_physics.Entities.IsCurrent(record)
&& ReferenceEquals(record.Projectile, projectile)
&& ReferenceEquals(record.PhysicsBody, projectile.Body)
&& projectile.PredictionAuthorityVersion
== predictionAuthorityVersion
&& (externalOwnerValid?.Invoke() ?? true);
private bool TryGetCurrent(
RuntimeEntityRecord record,
Func<bool>? externalOwnerValid,
out RuntimeProjectile projectile)
{
if (_physics.Entities.IsCurrent(record)
&& record.Projectile is RuntimeProjectile current
&& ReferenceEquals(record.PhysicsBody, current.Body)
&& (externalOwnerValid?.Invoke() ?? true))
{
projectile = current;
return true;
}
projectile = null!;
return false;
}
private static Vector3 CellLocalFromWorld(
Vector3 worldPosition,
uint cellId,
int liveCenterX,
int liveCenterY)
{
int landblockX = (int)((cellId >> 24) & 0xFFu);
int landblockY = (int)((cellId >> 16) & 0xFFu);
return worldPosition - new Vector3(
(landblockX - liveCenterX) * 192f,
(landblockY - liveCenterY) * 192f,
0f);
}
private static void Activate(PhysicsBody body, double currentTime)
{
if ((body.State & PhysicsStateFlags.Static) != 0)
return;
if ((body.TransientState & TransientStateFlags.Active) == 0)
body.LastUpdateTime = currentTime;
body.TransientState |= TransientStateFlags.Active;
}
private static void Deactivate(PhysicsBody body) =>
body.TransientState &= ~TransientStateFlags.Active;
private static bool IsFinite(Vector3 value) =>
float.IsFinite(value.X)
&& float.IsFinite(value.Y)
&& float.IsFinite(value.Z);
}