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:
parent
fce8e7b18e
commit
2aee33569f
19 changed files with 1255 additions and 335 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue