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(
|
var projectileController = new ProjectileController(
|
||||||
liveEntities,
|
liveEntities,
|
||||||
d.PhysicsEngine,
|
|
||||||
new DatProjectileSetupResolver(content.Dats, d.DatLock),
|
new DatProjectileSetupResolver(content.Dats, d.DatLock),
|
||||||
new EntityRootPosePublisher(d.EffectPoses),
|
new EntityRootPosePublisher(d.EffectPoses),
|
||||||
d.WorldOrigin)
|
d.WorldOrigin)
|
||||||
|
|
|
||||||
|
|
@ -35,51 +35,30 @@ internal sealed class DatProjectileSetupResolver : IProjectileSetupResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update-thread presentation owner for live retail missiles. Logical identity,
|
/// Update-thread presentation adapter for live retail missiles. Runtime owns
|
||||||
/// generation, accepted network state, and teardown remain owned by
|
/// logical identity, the projectile component, prediction versions, the
|
||||||
/// <see cref="LiveEntityRuntime"/>; this controller only advances the
|
/// canonical body/cell, and the split simulation transaction. This adapter
|
||||||
/// <see cref="PhysicsBody"/> attached to that canonical record and commits its
|
/// prepares the DAT Setup sphere, preserves animation-hook ordering, and
|
||||||
/// frame through the existing <see cref="WorldEntity"/> projection.
|
/// projects committed frames into <see cref="WorldEntity"/> and effect poses.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Retail anchors are <c>CPhysicsObj::update_object</c> <c>0x00515D10</c>,
|
/// Retail anchors are <c>CPhysicsObj::update_object</c> <c>0x00515D10</c>,
|
||||||
/// <c>CPhysicsObj::UpdateObjectInternal</c> <c>0x005156B0</c>, and
|
/// <c>CPhysicsObj::UpdateObjectInternal</c> <c>0x005156B0</c>, and
|
||||||
/// <c>CPhysicsObj::SetPositionInternal</c> <c>0x00515330</c>. The pure
|
/// <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
|
/// ACE remains authoritative for collision outcomes, damage, effects, and
|
||||||
/// deletion; no such events are synthesized here.
|
/// deletion; no such events are synthesized here.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
internal sealed class ProjectileController
|
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(
|
internal readonly record struct QuantumStep(
|
||||||
LiveEntityRecord Record,
|
LiveEntityRecord Record,
|
||||||
Runtime Runtime,
|
|
||||||
WorldEntity Entity,
|
WorldEntity Entity,
|
||||||
ProjectileQuantumPreparation Preparation,
|
RuntimeProjectilePhysicsCommit RuntimeCommit);
|
||||||
ulong PredictionAuthorityVersion);
|
|
||||||
|
|
||||||
private readonly LiveEntityRuntime _liveEntities;
|
private readonly LiveEntityRuntime _liveEntities;
|
||||||
private readonly ProjectilePhysicsStepper _stepper;
|
private readonly RuntimeProjectilePhysicsUpdater _runtimeUpdater;
|
||||||
private readonly ShadowObjectRegistry _shadows;
|
private readonly ShadowObjectRegistry _shadows;
|
||||||
private readonly IProjectileSetupResolver? _setupResolver;
|
private readonly IProjectileSetupResolver? _setupResolver;
|
||||||
private readonly IEntityRootPosePublisher? _rootPoses;
|
private readonly IEntityRootPosePublisher? _rootPoses;
|
||||||
|
|
@ -89,15 +68,14 @@ internal sealed class ProjectileController
|
||||||
|
|
||||||
internal ProjectileController(
|
internal ProjectileController(
|
||||||
LiveEntityRuntime liveEntities,
|
LiveEntityRuntime liveEntities,
|
||||||
PhysicsEngine physicsEngine,
|
|
||||||
IProjectileSetupResolver? setupResolver = null,
|
IProjectileSetupResolver? setupResolver = null,
|
||||||
IEntityRootPosePublisher? rootPoses = null,
|
IEntityRootPosePublisher? rootPoses = null,
|
||||||
LiveWorldOriginState? origin = null)
|
LiveWorldOriginState? origin = null)
|
||||||
{
|
{
|
||||||
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
|
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
|
||||||
ArgumentNullException.ThrowIfNull(physicsEngine);
|
_runtimeUpdater = new RuntimeProjectilePhysicsUpdater(
|
||||||
_stepper = new ProjectilePhysicsStepper(physicsEngine);
|
liveEntities.Physics);
|
||||||
_shadows = physicsEngine.ShadowObjects;
|
_shadows = liveEntities.Physics.Engine.ShadowObjects;
|
||||||
_setupResolver = setupResolver;
|
_setupResolver = setupResolver;
|
||||||
_rootPoses = rootPoses;
|
_rootPoses = rootPoses;
|
||||||
_origin = origin;
|
_origin = origin;
|
||||||
|
|
@ -107,7 +85,7 @@ internal sealed class ProjectileController
|
||||||
internal Action<string>? DiagnosticSink { get; set; }
|
internal Action<string>? DiagnosticSink { get; set; }
|
||||||
|
|
||||||
internal int Count => _liveEntities.Records.Count(
|
internal int Count => _liveEntities.Records.Count(
|
||||||
static record => record.ProjectileRuntime is Runtime);
|
static record => record.ProjectileRuntime is not null);
|
||||||
|
|
||||||
internal bool CanAcceptVectorPayload(
|
internal bool CanAcceptVectorPayload(
|
||||||
uint serverGuid,
|
uint serverGuid,
|
||||||
|
|
@ -167,7 +145,7 @@ internal sealed class ProjectileController
|
||||||
|| (record.FinalPhysicsState & PhysicsStateFlags.Missile) == 0)
|
|| (record.FinalPhysicsState & PhysicsStateFlags.Missile) == 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (record.ProjectileRuntime is Runtime retained)
|
if (record.ProjectileRuntime is RuntimeProjectile retained)
|
||||||
{
|
{
|
||||||
retained.Body.State = record.FinalPhysicsState;
|
retained.Body.State = record.FinalPhysicsState;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -261,7 +239,7 @@ internal sealed class ProjectileController
|
||||||
// Claim the incarnation's one CPhysicsObj before Rebucket can
|
// Claim the incarnation's one CPhysicsObj before Rebucket can
|
||||||
// publish visibility callbacks. A callback that also needs a body
|
// publish visibility callbacks. A callback that also needs a body
|
||||||
// must observe and adopt this same instance, never race a private
|
// 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(
|
body = _liveEntities.GetOrCreatePhysicsBody(
|
||||||
record.ServerGuid,
|
record.ServerGuid,
|
||||||
_ =>
|
_ =>
|
||||||
|
|
@ -308,7 +286,7 @@ internal sealed class ProjectileController
|
||||||
// the one projectile component before this outer call resumes. Adopt
|
// the one projectile component before this outer call resumes. Adopt
|
||||||
// that component; never replace it with a second Runtime that merely
|
// that component; never replace it with a second Runtime that merely
|
||||||
// happens to share the canonical CPhysicsObj body.
|
// happens to share the canonical CPhysicsObj body.
|
||||||
if (currentRecord.ProjectileRuntime is Runtime concurrentRuntime)
|
if (currentRecord.ProjectileRuntime is RuntimeProjectile concurrentRuntime)
|
||||||
return ReferenceEquals(concurrentRuntime.Body, body);
|
return ReferenceEquals(concurrentRuntime.Body, body);
|
||||||
|
|
||||||
// A newer Position can legally arrive from a projection callback
|
// 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.
|
// canonical body frame, never the pre-callback cell captured above.
|
||||||
canonicalCellId = body.CellPosition.ObjCellId;
|
canonicalCellId = body.CellPosition.ObjCellId;
|
||||||
|
|
||||||
var runtime = new Runtime(
|
RuntimeProjectile runtime = (RuntimeProjectile)
|
||||||
record.Generation,
|
_liveEntities.BindProjectileRuntime(
|
||||||
body,
|
record.ServerGuid,
|
||||||
sphere);
|
body,
|
||||||
_liveEntities.SetProjectileRuntime(record.ServerGuid, runtime);
|
sphere,
|
||||||
|
() => _liveEntities.IsCurrentRecord(record)
|
||||||
|
&& ReferenceEquals(record.WorldEntity, entity)
|
||||||
|
&& ReferenceEquals(record.PhysicsBody, body));
|
||||||
|
|
||||||
if (HasVisibleCell(record) && !IsHidden(record))
|
if (HasVisibleCell(record) && !IsHidden(record))
|
||||||
{
|
{
|
||||||
|
|
@ -374,7 +355,7 @@ internal sealed class ProjectileController
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(expectedRecord);
|
ArgumentNullException.ThrowIfNull(expectedRecord);
|
||||||
uint serverGuid = expectedRecord.ServerGuid;
|
uint serverGuid = expectedRecord.ServerGuid;
|
||||||
if (!TryGetCurrent(serverGuid, out LiveEntityRecord record, out Runtime runtime))
|
if (!TryGetCurrent(serverGuid, out LiveEntityRecord record, out RuntimeProjectile runtime))
|
||||||
return false;
|
return false;
|
||||||
if (!ReferenceEquals(record, expectedRecord)
|
if (!ReferenceEquals(record, expectedRecord)
|
||||||
|| record.VectorAuthorityVersion != expectedVectorAuthorityVersion
|
|| record.VectorAuthorityVersion != expectedVectorAuthorityVersion
|
||||||
|
|
@ -400,17 +381,19 @@ internal sealed class ProjectileController
|
||||||
}
|
}
|
||||||
_lastFiniteGameTime = currentTime;
|
_lastFiniteGameTime = currentTime;
|
||||||
|
|
||||||
runtime.InvalidatePrediction();
|
return _runtimeUpdater.ApplyAuthoritativeVector(
|
||||||
if (!_liveEntities.TryCommitAuthoritativeVector(
|
record.Canonical,
|
||||||
record,
|
expectedVectorAuthorityVersion,
|
||||||
runtime.Body,
|
expectedVelocityAuthorityVersion,
|
||||||
velocity,
|
velocity,
|
||||||
angularVelocity,
|
angularVelocity,
|
||||||
currentTime))
|
currentTime,
|
||||||
{
|
() => TryGetCurrent(
|
||||||
return true;
|
serverGuid,
|
||||||
}
|
out LiveEntityRecord current,
|
||||||
return true;
|
out RuntimeProjectile currentRuntime)
|
||||||
|
&& ReferenceEquals(current, record)
|
||||||
|
&& ReferenceEquals(currentRuntime, runtime));
|
||||||
}
|
}
|
||||||
|
|
||||||
internal bool ApplyAuthoritativeState(
|
internal bool ApplyAuthoritativeState(
|
||||||
|
|
@ -453,34 +436,22 @@ internal sealed class ProjectileController
|
||||||
DiagnosticSink?.Invoke(
|
DiagnosticSink?.Invoke(
|
||||||
$"Ignored invalid State update clock for live object 0x{serverGuid:X8}; state remains authoritative.");
|
$"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();
|
return _runtimeUpdater.ApplyAuthoritativeState(
|
||||||
bool wasMissile =
|
record.Canonical,
|
||||||
(runtime.Body.State & PhysicsStateFlags.Missile) != 0;
|
expectedStateAuthorityVersion,
|
||||||
runtime.Body.State = state;
|
state,
|
||||||
if ((state & PhysicsStateFlags.Missile) != 0 && !wasMissile)
|
effectiveClock,
|
||||||
{
|
liveCenterX,
|
||||||
// Ownership transfers back from generic remote motion to the
|
liveCenterY,
|
||||||
// absolute projectile clock. State alone must not activate,
|
() => _liveEntities.TryGetRecord(
|
||||||
// but an already-active body keeps moving from now rather
|
serverGuid,
|
||||||
// than replaying the ordinary-motion interval. A malformed
|
out LiveEntityRecord current)
|
||||||
// local receipt clock is not retail state and cannot clear
|
&& ReferenceEquals(current, record)
|
||||||
// Active; translate to the last finite App clock instead.
|
&& ReferenceEquals(
|
||||||
runtime.Body.LastUpdateTime = effectiveClock;
|
current.ProjectileRuntime,
|
||||||
if (record.FullCellId != 0)
|
runtime));
|
||||||
{
|
|
||||||
runtime.Body.SnapToCell(
|
|
||||||
record.FullCellId,
|
|
||||||
runtime.Body.Position,
|
|
||||||
CellLocalFromWorld(
|
|
||||||
runtime.Body.Position,
|
|
||||||
record.FullCellId,
|
|
||||||
liveCenterX,
|
|
||||||
liveCenterY));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetState applies to the canonical CPhysicsObj independently of
|
// SetState applies to the canonical CPhysicsObj independently of
|
||||||
|
|
@ -552,7 +523,7 @@ internal sealed class ProjectileController
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(expectedRecord);
|
ArgumentNullException.ThrowIfNull(expectedRecord);
|
||||||
uint serverGuid = expectedRecord.ServerGuid;
|
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)
|
|| !ReferenceEquals(record, expectedRecord)
|
||||||
|| record.PositionAuthorityVersion != expectedPositionAuthorityVersion
|
|| record.PositionAuthorityVersion != expectedPositionAuthorityVersion
|
||||||
|| (record.FinalPhysicsState & PhysicsStateFlags.Missile) == 0
|
|| (record.FinalPhysicsState & PhysicsStateFlags.Missile) == 0
|
||||||
|
|
@ -574,76 +545,39 @@ internal sealed class ProjectileController
|
||||||
}
|
}
|
||||||
_lastFiniteGameTime = currentTime;
|
_lastFiniteGameTime = currentTime;
|
||||||
|
|
||||||
PhysicsBody body = runtime.Body;
|
bool ExternalOwnerValid() =>
|
||||||
runtime.InvalidatePrediction();
|
TryGetCurrent(
|
||||||
bool wasInWorld = body.InWorld;
|
serverGuid,
|
||||||
body.Orientation = orientation;
|
out LiveEntityRecord current,
|
||||||
body.SnapToCell(fullCellId, worldPosition, cellLocalPosition);
|
out RuntimeProjectile currentRuntime)
|
||||||
body.State = record.FinalPhysicsState;
|
&& ReferenceEquals(current, record)
|
||||||
// PositionPack::UnPack initializes an absent velocity to zero and
|
&& ReferenceEquals(currentRuntime, runtime)
|
||||||
// MoveOrTeleport installs that exact vector through set_velocity.
|
&& ReferenceEquals(current.WorldEntity, entity)
|
||||||
// Apply it to this retained CPhysicsObj before any callback can
|
&& current.PositionAuthorityVersion
|
||||||
// displace the incarnation; a replacement record owns another body.
|
== expectedPositionAuthorityVersion;
|
||||||
if (record.VelocityAuthorityVersion == expectedVelocityAuthorityVersion
|
return _runtimeUpdater.ApplyAuthoritativePosition(
|
||||||
&& !_liveEntities.TryCommitAuthoritativeVelocity(
|
record.Canonical,
|
||||||
record,
|
expectedPositionAuthorityVersion,
|
||||||
body,
|
expectedVelocityAuthorityVersion,
|
||||||
velocity,
|
worldPosition,
|
||||||
currentTime))
|
cellLocalPosition,
|
||||||
{
|
orientation,
|
||||||
return true;
|
velocity,
|
||||||
}
|
fullCellId,
|
||||||
uint canonicalCellId = body.CellPosition.ObjCellId;
|
currentTime,
|
||||||
|
liveCenterX,
|
||||||
entity.SetPosition(worldPosition);
|
liveCenterY,
|
||||||
entity.Rotation = orientation;
|
snapshot =>
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
// prepare_to_enter_world rebases the CPhysicsObj timestamp in
|
if (!ExternalOwnerValid())
|
||||||
// the same operation that restores Active. The record clock is
|
return false;
|
||||||
// canonical, but keep this legacy body field synchronized for
|
entity.SetPosition(snapshot.Position);
|
||||||
// the absolute-time Core API and diagnostics.
|
entity.Rotation = snapshot.Orientation;
|
||||||
body.LastUpdateTime = currentTime;
|
entity.ParentCellId = snapshot.FullCellId;
|
||||||
Activate(body, currentTime);
|
_rootPoses?.UpdateRoot(entity);
|
||||||
}
|
return ExternalOwnerValid();
|
||||||
body.InWorld = true;
|
},
|
||||||
ShadowPositionSynchronizer.Sync(
|
ExternalOwnerValid);
|
||||||
_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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -660,7 +594,7 @@ internal sealed class ProjectileController
|
||||||
|
|
||||||
internal bool TryGetBody(uint serverGuid, out PhysicsBody body)
|
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;
|
body = runtime.Body;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -672,8 +606,7 @@ internal sealed class ProjectileController
|
||||||
internal bool LeaveWorld(LiveEntityRecord record)
|
internal bool LeaveWorld(LiveEntityRecord record)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(record);
|
ArgumentNullException.ThrowIfNull(record);
|
||||||
if (record.ProjectileRuntime is not Runtime runtime
|
if (record.ProjectileRuntime is not RuntimeProjectile runtime
|
||||||
|| runtime.Generation != record.Generation
|
|
||||||
|| record.WorldEntity is not { } entity)
|
|| record.WorldEntity is not { } entity)
|
||||||
return false;
|
return false;
|
||||||
SuspendOutsideWorld(runtime, entity.Id);
|
SuspendOutsideWorld(runtime, entity.Id);
|
||||||
|
|
@ -712,8 +645,7 @@ internal sealed class ProjectileController
|
||||||
_liveEntities.CopySpatialProjectileRecordsTo(_spatialProjectileSnapshot);
|
_liveEntities.CopySpatialProjectileRecordsTo(_spatialProjectileSnapshot);
|
||||||
foreach (LiveEntityRecord record in _spatialProjectileSnapshot)
|
foreach (LiveEntityRecord record in _spatialProjectileSnapshot)
|
||||||
{
|
{
|
||||||
if (record.ProjectileRuntime is not Runtime runtime
|
if (record.ProjectileRuntime is not RuntimeProjectile runtime
|
||||||
|| runtime.Generation != record.Generation
|
|
||||||
|| !_liveEntities.IsCurrentSpatialProjectile(record, runtime)
|
|| !_liveEntities.IsCurrentSpatialProjectile(record, runtime)
|
||||||
|| record.WorldEntity is not { } entity)
|
|| record.WorldEntity is not { } entity)
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -847,30 +779,34 @@ internal sealed class ProjectileController
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(record);
|
ArgumentNullException.ThrowIfNull(record);
|
||||||
step = default;
|
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)
|
|| !ReferenceEquals(record, current)
|
||||||
|| current.WorldEntity is not { } entity
|
|| current.WorldEntity is not { } entity
|
||||||
|| IsHidden(current)
|
|| IsHidden(current)
|
||||||
|| !HasVisibleCell(current))
|
|| !HasVisibleCell(current))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
runtime.Body.State = current.FinalPhysicsState;
|
bool ExternalOwnerValid() =>
|
||||||
bool isParented = current.Snapshot.ParentGuid is not null
|
_liveEntities.IsCurrentRecord(current)
|
||||||
|| current.Snapshot.Physics?.Parent is not null;
|
&& ReferenceEquals(current.WorldEntity, entity)
|
||||||
ProjectileQuantumPreparation preparation = _stepper.BeginQuantum(
|
&& ReferenceEquals(current.ProjectileRuntime, runtime);
|
||||||
runtime.Body,
|
if (!_runtimeUpdater.TryBegin(
|
||||||
quantum,
|
current.Canonical,
|
||||||
current.FullCellId,
|
quantum,
|
||||||
runtime.CollisionSphere,
|
current.ObjectClockEpoch,
|
||||||
isParented);
|
ExternalOwnerValid,
|
||||||
if (!preparation.Simulated)
|
out RuntimeProjectilePhysicsCommit runtimeCommit))
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
step = new QuantumStep(
|
step = new QuantumStep(
|
||||||
current,
|
current,
|
||||||
runtime,
|
|
||||||
entity,
|
entity,
|
||||||
preparation,
|
runtimeCommit);
|
||||||
runtime.PredictionAuthorityVersion);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -880,89 +816,42 @@ internal sealed class ProjectileController
|
||||||
int liveCenterY)
|
int liveCenterY)
|
||||||
{
|
{
|
||||||
LiveEntityRecord current = step.Record;
|
LiveEntityRecord current = step.Record;
|
||||||
Runtime runtime = step.Runtime;
|
|
||||||
WorldEntity entity = step.Entity;
|
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))
|
if (!IsCurrentQuantumIdentity(step))
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
QuantumStep exactStep = step;
|
||||||
|
|
||||||
if (HasVisibleCell(current))
|
return _runtimeUpdater.Complete(
|
||||||
{
|
exactStep.RuntimeCommit,
|
||||||
ShadowPositionSynchronizer.Sync(
|
liveCenterX,
|
||||||
_shadows,
|
liveCenterY,
|
||||||
entity.Id,
|
snapshot =>
|
||||||
runtime.Body.Position,
|
{
|
||||||
runtime.Body.Orientation,
|
if (!IsCurrentQuantumIdentity(exactStep))
|
||||||
current.FullCellId,
|
return false;
|
||||||
liveCenterX,
|
entity.SetPosition(snapshot.Position);
|
||||||
liveCenterY);
|
entity.Rotation = snapshot.Orientation;
|
||||||
_rootPoses?.UpdateRoot(entity);
|
entity.ParentCellId = snapshot.FullCellId;
|
||||||
if (!IsCurrentQuantumIdentity(step))
|
if (HasVisibleCell(current))
|
||||||
return false;
|
_rootPoses?.UpdateRoot(entity);
|
||||||
}
|
return IsCurrentQuantumIdentity(exactStep);
|
||||||
else
|
});
|
||||||
{
|
|
||||||
SuspendOutsideWorld(runtime, entity.Id);
|
|
||||||
}
|
|
||||||
return IsCurrentQuantumIdentity(step);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsCurrentQuantumStep(in QuantumStep step) =>
|
|
||||||
IsCurrentQuantumIdentity(step)
|
|
||||||
&& _liveEntities.IsCurrentSpatialProjectile(step.Record, step.Runtime);
|
|
||||||
|
|
||||||
private bool IsCurrentQuantumIdentity(in QuantumStep step) =>
|
private bool IsCurrentQuantumIdentity(in QuantumStep step) =>
|
||||||
TryGetCurrent(
|
TryGetCurrent(
|
||||||
step.Record.ServerGuid,
|
step.Record.ServerGuid,
|
||||||
out LiveEntityRecord current,
|
out LiveEntityRecord current,
|
||||||
out Runtime runtime)
|
out RuntimeProjectile runtime)
|
||||||
&& ReferenceEquals(current, step.Record)
|
&& ReferenceEquals(current, step.Record)
|
||||||
&& ReferenceEquals(runtime, step.Runtime)
|
|
||||||
&& ReferenceEquals(current.WorldEntity, step.Entity)
|
&& ReferenceEquals(current.WorldEntity, step.Entity)
|
||||||
|
&& ReferenceEquals(runtime, step.RuntimeCommit.Projectile)
|
||||||
&& runtime.PredictionAuthorityVersion
|
&& runtime.PredictionAuthorityVersion
|
||||||
== step.PredictionAuthorityVersion;
|
== step.RuntimeCommit.PredictionAuthorityVersion;
|
||||||
|
|
||||||
private void OnProjectionVisibilityChanged(LiveEntityRecord record, bool visible)
|
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
|
|| record.WorldEntity is not { } entity
|
||||||
|| !_liveEntities.TryGetRecord(record.ServerGuid, out LiveEntityRecord current)
|
|| !_liveEntities.TryGetRecord(record.ServerGuid, out LiveEntityRecord current)
|
||||||
|| !ReferenceEquals(current, record)
|
|| !ReferenceEquals(current, record)
|
||||||
|
|
@ -1020,22 +909,13 @@ internal sealed class ProjectileController
|
||||||
private static bool IsHidden(LiveEntityRecord record) =>
|
private static bool IsHidden(LiveEntityRecord record) =>
|
||||||
(record.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0;
|
(record.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0;
|
||||||
|
|
||||||
private void SuspendOutsideWorld(Runtime runtime, uint localEntityId)
|
private void SuspendOutsideWorld(RuntimeProjectile runtime, uint localEntityId)
|
||||||
{
|
{
|
||||||
runtime.Body.InWorld = false;
|
runtime.Body.InWorld = false;
|
||||||
Deactivate(runtime.Body);
|
Deactivate(runtime.Body);
|
||||||
_shadows.Suspend(localEntityId);
|
_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) =>
|
private static void Deactivate(PhysicsBody body) =>
|
||||||
body.TransientState &= ~TransientStateFlags.Active;
|
body.TransientState &= ~TransientStateFlags.Active;
|
||||||
|
|
||||||
|
|
@ -1089,11 +969,10 @@ internal sealed class ProjectileController
|
||||||
private bool TryGetCurrent(
|
private bool TryGetCurrent(
|
||||||
uint serverGuid,
|
uint serverGuid,
|
||||||
out LiveEntityRecord record,
|
out LiveEntityRecord record,
|
||||||
out Runtime runtime)
|
out RuntimeProjectile runtime)
|
||||||
{
|
{
|
||||||
if (_liveEntities.TryGetRecord(serverGuid, out record!)
|
if (_liveEntities.TryGetRecord(serverGuid, out record!)
|
||||||
&& record.ProjectileRuntime is Runtime found
|
&& record.ProjectileRuntime is RuntimeProjectile found)
|
||||||
&& found.Generation == record.Generation)
|
|
||||||
{
|
{
|
||||||
runtime = found;
|
runtime = found;
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -83,8 +83,8 @@ internal sealed class LiveEntityAnimationScheduler
|
||||||
|
|
||||||
LiveEntityAnimationState? animation = record.AnimationRuntime as LiveEntityAnimationState;
|
LiveEntityAnimationState? animation = record.AnimationRuntime as LiveEntityAnimationState;
|
||||||
RemoteMotion? remote = record.RemoteMotionRuntime as RemoteMotion;
|
RemoteMotion? remote = record.RemoteMotionRuntime as RemoteMotion;
|
||||||
ProjectileController.Runtime? projectile =
|
RuntimeProjectile? projectile =
|
||||||
record.ProjectileRuntime as ProjectileController.Runtime;
|
record.ProjectileRuntime as RuntimeProjectile;
|
||||||
ulong objectClockEpoch = record.ObjectClockEpoch;
|
ulong objectClockEpoch = record.ObjectClockEpoch;
|
||||||
|
|
||||||
if (!IsCurrent(
|
if (!IsCurrent(
|
||||||
|
|
@ -167,7 +167,7 @@ internal sealed class LiveEntityAnimationScheduler
|
||||||
WorldEntity entity,
|
WorldEntity entity,
|
||||||
LiveEntityAnimationState? animation,
|
LiveEntityAnimationState? animation,
|
||||||
RemoteMotion? remote,
|
RemoteMotion? remote,
|
||||||
ProjectileController.Runtime? projectile,
|
RuntimeProjectile? projectile,
|
||||||
float elapsedSeconds,
|
float elapsedSeconds,
|
||||||
Vector3? playerPosition,
|
Vector3? playerPosition,
|
||||||
bool localHiddenPartPoseDirty,
|
bool localHiddenPartPoseDirty,
|
||||||
|
|
@ -508,7 +508,7 @@ internal sealed class LiveEntityAnimationScheduler
|
||||||
WorldEntity entity,
|
WorldEntity entity,
|
||||||
LiveEntityAnimationState? animation,
|
LiveEntityAnimationState? animation,
|
||||||
RemoteMotion? remote,
|
RemoteMotion? remote,
|
||||||
ProjectileController.Runtime? projectile,
|
RuntimeProjectile? projectile,
|
||||||
ulong objectClockEpoch) =>
|
ulong objectClockEpoch) =>
|
||||||
runtime.IsCurrentSpatialRootObject(record)
|
runtime.IsCurrentSpatialRootObject(record)
|
||||||
&& record.ObjectClockEpoch == objectClockEpoch
|
&& record.ObjectClockEpoch == objectClockEpoch
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,6 @@ public interface ILiveEntityAnimationRuntime
|
||||||
uint CurrentMotion { get; }
|
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>
|
/// <summary>Marker for the DAT-driven effect profile added by the effect phase.</summary>
|
||||||
public interface ILiveEntityEffectProfile { }
|
public interface ILiveEntityEffectProfile { }
|
||||||
|
|
||||||
|
|
@ -257,7 +251,7 @@ public sealed class LiveEntityRecord
|
||||||
get => Canonical.RequiresRemotePlacementRuntime;
|
get => Canonical.RequiresRemotePlacementRuntime;
|
||||||
set => _directory.SetRequiresRemotePlacementRuntime(Canonical, value);
|
set => _directory.SetRequiresRemotePlacementRuntime(Canonical, value);
|
||||||
}
|
}
|
||||||
public ILiveEntityProjectileRuntime? ProjectileRuntime { get; internal set; }
|
public IRuntimeProjectile? ProjectileRuntime => Canonical.Projectile;
|
||||||
public ILiveEntityEffectProfile? EffectProfile { get; internal set; }
|
public ILiveEntityEffectProfile? EffectProfile { get; internal set; }
|
||||||
public bool ResourcesRegistered { get; internal set; }
|
public bool ResourcesRegistered { get; internal set; }
|
||||||
public bool IsSpatiallyProjected { 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
|
// projection. Hidden and Frozen remain members: their state controls what
|
||||||
// each retail update path advances after the O(active) lookup.
|
// each retail update path advances after the O(active) lookup.
|
||||||
private readonly Dictionary<RuntimeEntityKey, ILiveEntityAnimationRuntime> _spatialAnimations = new();
|
private readonly Dictionary<RuntimeEntityKey, ILiveEntityAnimationRuntime> _spatialAnimations = new();
|
||||||
private readonly Dictionary<RuntimeEntityKey, ILiveEntityProjectileRuntime> _spatialProjectiles = new();
|
|
||||||
private readonly List<RuntimeEntityRecord> _spatialRootCanonicalScratch = new();
|
private readonly List<RuntimeEntityRecord> _spatialRootCanonicalScratch = new();
|
||||||
private readonly List<RuntimeEntityRecord> _spatialRemoteCanonicalScratch = new();
|
private readonly List<RuntimeEntityRecord> _spatialRemoteCanonicalScratch = new();
|
||||||
private bool _isClearing;
|
private bool _isClearing;
|
||||||
|
|
@ -477,7 +470,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
||||||
}
|
}
|
||||||
internal int SpatialAnimationRuntimeCount => _spatialAnimations.Count;
|
internal int SpatialAnimationRuntimeCount => _spatialAnimations.Count;
|
||||||
internal int SpatialRemoteMotionRuntimeCount => _physics.SpatialRemoteCount;
|
internal int SpatialRemoteMotionRuntimeCount => _physics.SpatialRemoteCount;
|
||||||
internal int SpatialProjectileRuntimeCount => _spatialProjectiles.Count;
|
internal int SpatialProjectileRuntimeCount => _physics.SpatialProjectileCount;
|
||||||
internal int SpatialRootObjectCount => _physics.SpatialRootCount;
|
internal int SpatialRootObjectCount => _physics.SpatialRootCount;
|
||||||
public ParentAttachmentState ParentAttachments => _directory.ParentAttachments;
|
public ParentAttachmentState ParentAttachments => _directory.ParentAttachments;
|
||||||
|
|
||||||
|
|
@ -1475,9 +1468,13 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
||||||
return true;
|
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)
|
if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
||||||
|| record.WorldEntity is null)
|
|| record.WorldEntity is null)
|
||||||
{
|
{
|
||||||
|
|
@ -1487,24 +1484,28 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
||||||
if (record.PhysicsBodyAcquisitionInProgress && record.PhysicsBody is null)
|
if (record.PhysicsBodyAcquisitionInProgress && record.PhysicsBody is null)
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"Live entity 0x{serverGuid:X8} cannot bind projectile physics during physics-body acquisition.");
|
$"Live entity 0x{serverGuid:X8} cannot bind projectile physics during physics-body acquisition.");
|
||||||
PhysicsBody candidateBody = runtime.Body;
|
|
||||||
if (record.PhysicsBody is { } canonicalBody
|
if (record.PhysicsBody is { } canonicalBody
|
||||||
&& !ReferenceEquals(canonicalBody, candidateBody))
|
&& !ReferenceEquals(canonicalBody, body))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"Live entity 0x{serverGuid:X8} cannot replace its canonical physics body within one incarnation.");
|
$"Live entity 0x{serverGuid:X8} cannot replace its canonical physics body within one incarnation.");
|
||||||
}
|
}
|
||||||
|
|
||||||
record.ProjectileRuntime = runtime;
|
IRuntimeProjectile runtime = _physics.BindProjectile(
|
||||||
record.PhysicsBody = candidateBody;
|
record.Canonical,
|
||||||
candidateBody.State = record.FinalPhysicsState;
|
body,
|
||||||
SynchronizePhysicsBodyActiveState(record);
|
collisionSphere,
|
||||||
|
() => IsCurrentRecord(record)
|
||||||
|
&& record.WorldEntity is { } entity
|
||||||
|
&& record.LocalEntityId == entity.Id
|
||||||
|
&& (externalOwnerValid?.Invoke() ?? true));
|
||||||
RefreshSpatialRuntimeIndexes(record);
|
RefreshSpatialRuntimeIndexes(record);
|
||||||
|
return runtime;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGetProjectileRuntime(
|
public bool TryGetProjectileRuntime(
|
||||||
uint serverGuid,
|
uint serverGuid,
|
||||||
out ILiveEntityProjectileRuntime runtime)
|
out IRuntimeProjectile runtime)
|
||||||
{
|
{
|
||||||
if (_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
if (_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
||||||
&& record.ProjectileRuntime is { } found)
|
&& record.ProjectileRuntime is { } found)
|
||||||
|
|
@ -1523,7 +1524,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
||||||
|| record.ProjectileRuntime is null)
|
|| record.ProjectileRuntime is null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
record.ProjectileRuntime = null;
|
_physics.ClearProjectile(record.Canonical);
|
||||||
RefreshSpatialRuntimeIndexes(record);
|
RefreshSpatialRuntimeIndexes(record);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -2021,11 +2022,11 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
||||||
ArgumentNullException.ThrowIfNull(destination);
|
ArgumentNullException.ThrowIfNull(destination);
|
||||||
destination.Clear();
|
destination.Clear();
|
||||||
|
|
||||||
foreach ((RuntimeEntityKey key, ILiveEntityProjectileRuntime runtime)
|
_physics.CopySpatialProjectilesTo(_spatialRootCanonicalScratch);
|
||||||
in _spatialProjectiles)
|
for (int i = 0; i < _spatialRootCanonicalScratch.Count; i++)
|
||||||
{
|
{
|
||||||
if (_projections.TryGet(key, out LiveEntityRecord? record)
|
RuntimeEntityRecord canonical = _spatialRootCanonicalScratch[i];
|
||||||
&& ReferenceEquals(record.ProjectileRuntime, runtime)
|
if (_projections.TryGet(canonical, out LiveEntityRecord? record)
|
||||||
&& HasSpatialRuntimeProjection(record))
|
&& HasSpatialRuntimeProjection(record))
|
||||||
{
|
{
|
||||||
destination.Add(record);
|
destination.Add(record);
|
||||||
|
|
@ -2035,12 +2036,9 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
||||||
|
|
||||||
internal bool IsCurrentSpatialProjectile(
|
internal bool IsCurrentSpatialProjectile(
|
||||||
LiveEntityRecord record,
|
LiveEntityRecord record,
|
||||||
ILiveEntityProjectileRuntime runtime) =>
|
IRuntimeProjectile runtime) =>
|
||||||
IsCurrentRecord(record)
|
IsCurrentRecord(record)
|
||||||
&& ReferenceEquals(record.ProjectileRuntime, runtime)
|
&& _physics.IsSpatialProjectile(record.Canonical, runtime)
|
||||||
&& record.ProjectionKey is { } key
|
|
||||||
&& _spatialProjectiles.TryGetValue(key, out var indexed)
|
|
||||||
&& ReferenceEquals(indexed, runtime)
|
|
||||||
&& HasSpatialRuntimeProjection(record);
|
&& HasSpatialRuntimeProjection(record);
|
||||||
|
|
||||||
public bool IsHidden(uint serverGuid) =>
|
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)
|
private void RemoveSpatialRuntimeIndexes(LiveEntityRecord record)
|
||||||
|
|
@ -2486,11 +2473,6 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
||||||
_spatialAnimations.Remove(key);
|
_spatialAnimations.Remove(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_spatialProjectiles.TryGetValue(key, out var indexedProjectile)
|
|
||||||
&& ReferenceEquals(indexedProjectile, record.ProjectileRuntime))
|
|
||||||
{
|
|
||||||
_spatialProjectiles.Remove(key);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSpatialVisibilityChanged(RuntimeEntityKey key, bool visible)
|
private void OnSpatialVisibilityChanged(RuntimeEntityKey key, bool visible)
|
||||||
|
|
@ -2681,7 +2663,6 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
||||||
}
|
}
|
||||||
|
|
||||||
_spatialAnimations.Clear();
|
_spatialAnimations.Clear();
|
||||||
_spatialProjectiles.Clear();
|
|
||||||
_physics.ClearSpatialWorksets();
|
_physics.ClearSpatialWorksets();
|
||||||
_projections.ClearConverged();
|
_projections.ClearConverged();
|
||||||
_spatial.ClearLiveEntityLifetimeState();
|
_spatial.ClearLiveEntityLifetimeState();
|
||||||
|
|
@ -2717,6 +2698,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
||||||
// a retryable tombstone until every external cleanup acknowledges
|
// a retryable tombstone until every external cleanup acknowledges
|
||||||
// success.
|
// success.
|
||||||
RemoveSpatialRuntimeIndexes(record);
|
RemoveSpatialRuntimeIndexes(record);
|
||||||
|
_physics.ClearProjectile(record.Canonical);
|
||||||
|
|
||||||
if (!record.RuntimeComponentsTeardownCompleted)
|
if (!record.RuntimeComponentsTeardownCompleted)
|
||||||
{
|
{
|
||||||
|
|
@ -2756,7 +2738,6 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
||||||
record.PhysicsHost = null;
|
record.PhysicsHost = null;
|
||||||
record.RequiresRemotePlacementRuntime = false;
|
record.RequiresRemotePlacementRuntime = false;
|
||||||
record.PhysicsBody = null;
|
record.PhysicsBody = null;
|
||||||
record.ProjectileRuntime = null;
|
|
||||||
record.EffectProfile = null;
|
record.EffectProfile = null;
|
||||||
record.IsSpatiallyProjected = false;
|
record.IsSpatiallyProjected = false;
|
||||||
record.IsSpatiallyVisible = false;
|
record.IsSpatiallyVisible = false;
|
||||||
|
|
|
||||||
|
|
@ -355,6 +355,22 @@ public sealed class RuntimeEntityDirectory
|
||||||
record.RemoteMotionBindingInProgress = value;
|
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(
|
public void SetRequiresRemotePlacementRuntime(
|
||||||
RuntimeEntityRecord record,
|
RuntimeEntityRecord record,
|
||||||
bool value)
|
bool value)
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,20 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
Events = new RuntimeEntityObjectEventStream(Entities, Objects);
|
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 RuntimeEntityDirectory Entities { get; }
|
||||||
public RuntimePhysicsState Physics { get; }
|
public RuntimePhysicsState Physics { get; }
|
||||||
public ClientObjectTable Objects { get; }
|
public ClientObjectTable Objects { get; }
|
||||||
|
|
@ -344,6 +358,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
Physics.RemoveSpatialProjection(canonical);
|
Physics.RemoveSpatialProjection(canonical);
|
||||||
Entities.SetRemoteMotion(canonical, null);
|
Entities.SetRemoteMotion(canonical, null);
|
||||||
Entities.SetRemoteMotionBindingInProgress(canonical, false);
|
Entities.SetRemoteMotionBindingInProgress(canonical, false);
|
||||||
|
Entities.SetProjectile(canonical, null);
|
||||||
|
Entities.SetProjectileBindingInProgress(canonical, false);
|
||||||
Entities.SetRequiresRemotePlacementRuntime(canonical, false);
|
Entities.SetRequiresRemotePlacementRuntime(canonical, false);
|
||||||
Entities.SetPhysicsHost(canonical, null);
|
Entities.SetPhysicsHost(canonical, null);
|
||||||
Entities.SetPhysicsBody(canonical, null);
|
Entities.SetPhysicsBody(canonical, null);
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,8 @@ public sealed class RuntimeEntityRecord
|
||||||
public bool PhysicsBodyAcquisitionInProgress { get; internal set; }
|
public bool PhysicsBodyAcquisitionInProgress { get; internal set; }
|
||||||
public IRuntimeRemoteMotion? RemoteMotion { get; internal set; }
|
public IRuntimeRemoteMotion? RemoteMotion { get; internal set; }
|
||||||
public bool RemoteMotionBindingInProgress { 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 bool RequiresRemotePlacementRuntime { get; internal set; }
|
||||||
public AcDream.Core.Physics.Motion.IPhysicsObjHost? PhysicsHost { get; internal set; }
|
public AcDream.Core.Physics.Motion.IPhysicsObjHost? PhysicsHost { get; internal set; }
|
||||||
public ulong PositionAuthorityVersion { get; private set; }
|
public ulong PositionAuthorityVersion { get; private set; }
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ public readonly record struct RuntimePhysicsOwnershipSnapshot(
|
||||||
int RetainedShadowRegistrationCount,
|
int RetainedShadowRegistrationCount,
|
||||||
int SpatialRootCount,
|
int SpatialRootCount,
|
||||||
int SpatialRemoteCount,
|
int SpatialRemoteCount,
|
||||||
|
int SpatialProjectileCount,
|
||||||
int CollisionAdmissionCount,
|
int CollisionAdmissionCount,
|
||||||
int CollisionGenerationCount,
|
int CollisionGenerationCount,
|
||||||
bool OwnsProductionDataCache,
|
bool OwnsProductionDataCache,
|
||||||
|
|
@ -64,6 +65,8 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
_spatialRoots = new();
|
_spatialRoots = new();
|
||||||
private readonly Dictionary<RuntimeEntityKey, IRuntimeRemoteMotion>
|
private readonly Dictionary<RuntimeEntityKey, IRuntimeRemoteMotion>
|
||||||
_spatialRemotes = new();
|
_spatialRemotes = new();
|
||||||
|
private readonly Dictionary<RuntimeEntityKey, IRuntimeProjectile>
|
||||||
|
_spatialProjectiles = new();
|
||||||
private readonly Dictionary<uint, ulong> _collisionGenerations = new();
|
private readonly Dictionary<uint, ulong> _collisionGenerations = new();
|
||||||
private readonly Dictionary<uint, RuntimeCollisionAdmission>
|
private readonly Dictionary<uint, RuntimeCollisionAdmission>
|
||||||
_collisionAdmissions = new();
|
_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; }
|
internal RuntimeEntityDirectory Entities { get; }
|
||||||
public PhysicsEngine Engine { get; }
|
public PhysicsEngine Engine { get; }
|
||||||
public PhysicsDataCache DataCache { get; }
|
public PhysicsDataCache DataCache { get; }
|
||||||
public int SpatialRootCount => _spatialRoots.Count;
|
public int SpatialRootCount => _spatialRoots.Count;
|
||||||
public int SpatialRemoteCount => _spatialRemotes.Count;
|
public int SpatialRemoteCount => _spatialRemotes.Count;
|
||||||
|
public int SpatialProjectileCount => _spatialProjectiles.Count;
|
||||||
|
|
||||||
public RuntimePhysicsOwnershipSnapshot CaptureOwnership() =>
|
public RuntimePhysicsOwnershipSnapshot CaptureOwnership() =>
|
||||||
new(
|
new(
|
||||||
|
|
@ -95,6 +109,7 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
Engine.ShadowObjects.RetainedRegistrationCount,
|
Engine.ShadowObjects.RetainedRegistrationCount,
|
||||||
_spatialRoots.Count,
|
_spatialRoots.Count,
|
||||||
_spatialRemotes.Count,
|
_spatialRemotes.Count,
|
||||||
|
_spatialProjectiles.Count,
|
||||||
_collisionAdmissions.Count,
|
_collisionAdmissions.Count,
|
||||||
_collisionGenerations.Count,
|
_collisionGenerations.Count,
|
||||||
ReferenceEquals(Engine.DataCache, DataCache),
|
ReferenceEquals(Engine.DataCache, DataCache),
|
||||||
|
|
@ -123,6 +138,10 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
_spatialRemotes[key] = remote;
|
_spatialRemotes[key] = remote;
|
||||||
else
|
else
|
||||||
_spatialRemotes.Remove(key);
|
_spatialRemotes.Remove(key);
|
||||||
|
if (record.Projectile is { } projectile)
|
||||||
|
_spatialProjectiles[key] = projectile;
|
||||||
|
else
|
||||||
|
_spatialProjectiles.Remove(key);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,6 +167,167 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
_spatialRemotes.Remove(key);
|
_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(
|
public void SetRemoteMotion(
|
||||||
RuntimeEntityRecord record,
|
RuntimeEntityRecord record,
|
||||||
IRuntimeRemoteMotion runtime,
|
IRuntimeRemoteMotion runtime,
|
||||||
|
|
@ -476,6 +656,7 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
_spatialRoots.Remove(key);
|
_spatialRoots.Remove(key);
|
||||||
}
|
}
|
||||||
RemoveSpatialRemote(record);
|
RemoveSpatialRemote(record);
|
||||||
|
RemoveSpatialProjectile(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsSpatialRoot(RuntimeEntityRecord record) =>
|
public bool IsSpatialRoot(RuntimeEntityRecord record) =>
|
||||||
|
|
@ -493,6 +674,15 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
&& _spatialRemotes.TryGetValue(key, out IRuntimeRemoteMotion? indexed)
|
&& _spatialRemotes.TryGetValue(key, out IRuntimeRemoteMotion? indexed)
|
||||||
&& ReferenceEquals(indexed, remote);
|
&& 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)
|
public void CopySpatialRootsTo(List<RuntimeEntityRecord> destination)
|
||||||
{
|
{
|
||||||
EnsureNotDisposed();
|
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(
|
internal bool CommitOrdinaryCell(
|
||||||
RuntimeEntityRecord record,
|
RuntimeEntityRecord record,
|
||||||
PhysicsBody body,
|
PhysicsBody body,
|
||||||
|
|
@ -551,10 +761,34 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
IsExactOwner);
|
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()
|
public void ClearSpatialWorksets()
|
||||||
{
|
{
|
||||||
EnsureNotDisposed();
|
EnsureNotDisposed();
|
||||||
_spatialRemotes.Clear();
|
_spatialRemotes.Clear();
|
||||||
|
_spatialProjectiles.Clear();
|
||||||
_spatialRoots.Clear();
|
_spatialRoots.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -672,6 +906,7 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
return;
|
return;
|
||||||
_spatialRemotes.Clear();
|
_spatialRemotes.Clear();
|
||||||
|
_spatialProjectiles.Clear();
|
||||||
_spatialRoots.Clear();
|
_spatialRoots.Clear();
|
||||||
_collisionAdmissions.Clear();
|
_collisionAdmissions.Clear();
|
||||||
_collisionGenerations.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() =>
|
private void EnsureNotDisposed() =>
|
||||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
|
||||||
|
|
@ -775,6 +1025,11 @@ public sealed class RuntimePhysicsState : IDisposable
|
||||||
private static uint CanonicalLandblock(uint value) =>
|
private static uint CanonicalLandblock(uint value) =>
|
||||||
(value & 0xFFFF0000u) | 0xFFFFu;
|
(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)
|
private static void SynchronizeBodyActiveState(RuntimeEntityRecord record)
|
||||||
{
|
{
|
||||||
if (record.PhysicsBody is not { } body)
|
if (record.PhysicsBody is not { } body)
|
||||||
|
|
|
||||||
40
src/AcDream.Runtime/Physics/RuntimeProjectile.cs
Normal file
40
src/AcDream.Runtime/Physics/RuntimeProjectile.cs
Normal 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++;
|
||||||
|
}
|
||||||
500
src/AcDream.Runtime/Physics/RuntimeProjectilePhysicsUpdater.cs
Normal file
500
src/AcDream.Runtime/Physics/RuntimeProjectilePhysicsUpdater.cs
Normal 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);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Runtime.Entities;
|
using AcDream.Runtime.Entities;
|
||||||
|
|
||||||
namespace AcDream.App.Tests;
|
namespace AcDream.App.Tests;
|
||||||
|
|
@ -20,6 +21,18 @@ internal static class LiveEntityRuntimeFixture
|
||||||
return new LiveEntityRuntime(spatial, resources, lifetime);
|
return new LiveEntityRuntime(spatial, resources, lifetime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static LiveEntityRuntime Create(
|
||||||
|
GpuWorldState spatial,
|
||||||
|
ILiveEntityResourceLifecycle resources,
|
||||||
|
PhysicsEngine physicsEngine,
|
||||||
|
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId)
|
||||||
|
{
|
||||||
|
var lifetime = new RuntimeEntityObjectLifetime(
|
||||||
|
physicsEngine,
|
||||||
|
firstLocalEntityId);
|
||||||
|
return new LiveEntityRuntime(spatial, resources, lifetime);
|
||||||
|
}
|
||||||
|
|
||||||
public static LiveEntityRuntime Create(
|
public static LiveEntityRuntime Create(
|
||||||
GpuWorldState spatial,
|
GpuWorldState spatial,
|
||||||
ILiveEntityResourceLifecycle resources,
|
ILiveEntityResourceLifecycle resources,
|
||||||
|
|
@ -47,4 +60,21 @@ internal static class LiveEntityRuntimeFixture
|
||||||
runtimeComponentLifecycle,
|
runtimeComponentLifecycle,
|
||||||
lifetime);
|
lifetime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static LiveEntityRuntime Create(
|
||||||
|
GpuWorldState spatial,
|
||||||
|
ILiveEntityResourceLifecycle resources,
|
||||||
|
ILiveEntityRuntimeComponentLifecycle runtimeComponentLifecycle,
|
||||||
|
PhysicsEngine physicsEngine,
|
||||||
|
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId)
|
||||||
|
{
|
||||||
|
var lifetime = new RuntimeEntityObjectLifetime(
|
||||||
|
physicsEngine,
|
||||||
|
firstLocalEntityId);
|
||||||
|
return new LiveEntityRuntime(
|
||||||
|
spatial,
|
||||||
|
resources,
|
||||||
|
runtimeComponentLifecycle,
|
||||||
|
lifetime);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,7 @@ public sealed class ProjectileControllerTests
|
||||||
seedCellId: CellA,
|
seedCellId: CellA,
|
||||||
isStatic: false);
|
isStatic: false);
|
||||||
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 2.0));
|
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 2.0));
|
||||||
ILiveEntityProjectileRuntime runtime = record.ProjectileRuntime!;
|
IRuntimeProjectile runtime = record.ProjectileRuntime!;
|
||||||
|
|
||||||
Assert.True(fixture.Controller.ApplyAuthoritativePosition(
|
Assert.True(fixture.Controller.ApplyAuthoritativePosition(
|
||||||
record,
|
record,
|
||||||
|
|
@ -339,6 +339,7 @@ public sealed class ProjectileControllerTests
|
||||||
|
|
||||||
Assert.Equal(0xAAB40001u, record.FullCellId);
|
Assert.Equal(0xAAB40001u, record.FullCellId);
|
||||||
Assert.Equal(0xAAB40001u, record.PhysicsBody!.CellPosition.ObjCellId);
|
Assert.Equal(0xAAB40001u, record.PhysicsBody!.CellPosition.ObjCellId);
|
||||||
|
Assert.Equal(record.PhysicsBody.Position, entity.Position);
|
||||||
Assert.False(record.IsSpatiallyVisible);
|
Assert.False(record.IsSpatiallyVisible);
|
||||||
Assert.True(record.IsSpatiallyProjected);
|
Assert.True(record.IsSpatiallyProjected);
|
||||||
Assert.False(record.PhysicsBody.InWorld);
|
Assert.False(record.PhysicsBody.InWorld);
|
||||||
|
|
@ -514,7 +515,7 @@ public sealed class ProjectileControllerTests
|
||||||
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
|
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(), 1.0));
|
||||||
Vector3 before = record.WorldEntity!.Position;
|
Vector3 before = record.WorldEntity!.Position;
|
||||||
|
|
||||||
ILiveEntityProjectileRuntime runtime = record.ProjectileRuntime!;
|
IRuntimeProjectile runtime = record.ProjectileRuntime!;
|
||||||
PhysicsBody body = runtime.Body;
|
PhysicsBody body = runtime.Body;
|
||||||
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
|
record.FinalPhysicsState = PhysicsStateFlags.ReportCollisions;
|
||||||
Assert.True(fixture.Controller.ApplyAuthoritativeState(
|
Assert.True(fixture.Controller.ApplyAuthoritativeState(
|
||||||
|
|
@ -565,7 +566,7 @@ public sealed class ProjectileControllerTests
|
||||||
cellLocalPosition: new Vector3(191f, 10f, 50f),
|
cellLocalPosition: new Vector3(191f, 10f, 50f),
|
||||||
velocity: new Vector3(40f, 0f, 0f));
|
velocity: new Vector3(40f, 0f, 0f));
|
||||||
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(0.5f), 0.0));
|
Assert.True(fixture.Controller.TryBind(record, ProjectileSetup(0.5f), 0.0));
|
||||||
ILiveEntityProjectileRuntime runtime = record.ProjectileRuntime!;
|
IRuntimeProjectile runtime = record.ProjectileRuntime!;
|
||||||
|
|
||||||
fixture.Controller.Tick(0.05, 0xA9, 0xB4, playerWorldPosition: null);
|
fixture.Controller.Tick(0.05, 0xA9, 0xB4, playerWorldPosition: null);
|
||||||
Assert.Equal(destinationCell, record.FullCellId);
|
Assert.Equal(destinationCell, record.FullCellId);
|
||||||
|
|
@ -988,7 +989,7 @@ public sealed class ProjectileControllerTests
|
||||||
Assert.Equal(MissileState, record.PhysicsBody!.State);
|
Assert.Equal(MissileState, record.PhysicsBody!.State);
|
||||||
Assert.True(double.IsFinite(record.PhysicsBody.LastUpdateTime));
|
Assert.True(double.IsFinite(record.PhysicsBody.LastUpdateTime));
|
||||||
|
|
||||||
ILiveEntityProjectileRuntime runtime = record.ProjectileRuntime!;
|
IRuntimeProjectile runtime = record.ProjectileRuntime!;
|
||||||
Assert.True(fixture.Controller.ApplyAuthoritativeState(
|
Assert.True(fixture.Controller.ApplyAuthoritativeState(
|
||||||
record, MissileState, 0.1, 1, 1));
|
record, MissileState, 0.1, 1, 1));
|
||||||
Assert.Same(runtime, record.ProjectileRuntime);
|
Assert.Same(runtime, record.ProjectileRuntime);
|
||||||
|
|
@ -1320,14 +1321,14 @@ public sealed class ProjectileControllerTests
|
||||||
LiveEntityRecord first = fixture.Spawn(instance: 7);
|
LiveEntityRecord first = fixture.Spawn(instance: 7);
|
||||||
Assert.True(fixture.Controller.TryBind(first, ProjectileSetup(), 1.0));
|
Assert.True(fixture.Controller.TryBind(first, ProjectileSetup(), 1.0));
|
||||||
WorldEntity firstEntity = first.WorldEntity!;
|
WorldEntity firstEntity = first.WorldEntity!;
|
||||||
ILiveEntityProjectileRuntime firstRuntime = first.ProjectileRuntime!;
|
IRuntimeProjectile firstRuntime = first.ProjectileRuntime!;
|
||||||
|
|
||||||
Assert.True(fixture.Live.UnregisterLiveEntity(
|
Assert.True(fixture.Live.UnregisterLiveEntity(
|
||||||
new DeleteObject.Parsed(Guid, InstanceSequence: 7),
|
new DeleteObject.Parsed(Guid, InstanceSequence: 7),
|
||||||
isLocalPlayer: false));
|
isLocalPlayer: false));
|
||||||
LiveEntityRecord second = fixture.Spawn(instance: 8);
|
LiveEntityRecord second = fixture.Spawn(instance: 8);
|
||||||
Assert.True(fixture.Controller.TryBind(second, ProjectileSetup(), 2.0));
|
Assert.True(fixture.Controller.TryBind(second, ProjectileSetup(), 2.0));
|
||||||
ILiveEntityProjectileRuntime secondRuntime = second.ProjectileRuntime!;
|
IRuntimeProjectile secondRuntime = second.ProjectileRuntime!;
|
||||||
|
|
||||||
Assert.False(fixture.Controller.TryBind(first, ProjectileSetup(), 3.0));
|
Assert.False(fixture.Controller.TryBind(first, ProjectileSetup(), 3.0));
|
||||||
|
|
||||||
|
|
@ -1457,12 +1458,14 @@ public sealed class ProjectileControllerTests
|
||||||
{
|
{
|
||||||
if (loadInitialLandblock)
|
if (loadInitialLandblock)
|
||||||
Spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
Spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||||
Live = LiveEntityRuntimeFixture.Create(Spatial, Resources);
|
|
||||||
Engine = physicsEngine ?? new PhysicsEngine();
|
Engine = physicsEngine ?? new PhysicsEngine();
|
||||||
|
Live = LiveEntityRuntimeFixture.Create(
|
||||||
|
Spatial,
|
||||||
|
Resources,
|
||||||
|
Engine);
|
||||||
Origin.Recenter(1, 1);
|
Origin.Recenter(1, 1);
|
||||||
Controller = new ProjectileController(
|
Controller = new ProjectileController(
|
||||||
Live,
|
Live,
|
||||||
Engine,
|
|
||||||
new SetupResolver(() => ResolvedSetup),
|
new SetupResolver(() => ResolvedSetup),
|
||||||
new RootPosePublisher(
|
new RootPosePublisher(
|
||||||
publishRootPose ?? new Action<WorldEntity>(
|
publishRootPose ?? new Action<WorldEntity>(
|
||||||
|
|
|
||||||
|
|
@ -71,14 +71,6 @@ public sealed class RemoteTeleportControllerTests
|
||||||
public void LeaveGround() { }
|
public void LeaveGround() { }
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class AlternatingProjectileRuntime(
|
|
||||||
PhysicsBody first,
|
|
||||||
PhysicsBody later) : ILiveEntityProjectileRuntime
|
|
||||||
{
|
|
||||||
private int _reads;
|
|
||||||
public PhysicsBody Body => _reads++ == 0 ? first : later;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PendingDestination_ParksAuthoritativeFrameThenResolvesContactOnLoad()
|
public void PendingDestination_ParksAuthoritativeFrameThenResolvesContactOnLoad()
|
||||||
{
|
{
|
||||||
|
|
@ -867,8 +859,10 @@ public sealed class RemoteTeleportControllerTests
|
||||||
Assert.Same(canonicalBody, record.PhysicsBody);
|
Assert.Same(canonicalBody, record.PhysicsBody);
|
||||||
Assert.Same(remote, record.RemoteMotionRuntime);
|
Assert.Same(remote, record.RemoteMotionRuntime);
|
||||||
|
|
||||||
var projectile = new AlternatingProjectileRuntime(canonicalBody, otherBody);
|
IRuntimeProjectile projectile = fixture.Live.BindProjectileRuntime(
|
||||||
fixture.Live.SetProjectileRuntime(RollbackFixture.Guid, projectile);
|
RollbackFixture.Guid,
|
||||||
|
canonicalBody,
|
||||||
|
new ProjectileCollisionSphere(Vector3.Zero, 0.25f));
|
||||||
|
|
||||||
Assert.Same(canonicalBody, record.PhysicsBody);
|
Assert.Same(canonicalBody, record.PhysicsBody);
|
||||||
Assert.Same(projectile, record.ProjectileRuntime);
|
Assert.Same(projectile, record.ProjectileRuntime);
|
||||||
|
|
|
||||||
|
|
@ -234,8 +234,8 @@ public sealed class LiveEntityAnimationSchedulerTests
|
||||||
var (live, record, animation) = BuildVisibleAnimatedWithPosFrames(
|
var (live, record, animation) = BuildVisibleAnimatedWithPosFrames(
|
||||||
RemoteGuid,
|
RemoteGuid,
|
||||||
rootOrigin: Vector3.Zero);
|
rootOrigin: Vector3.Zero);
|
||||||
var physics = new PhysicsEngine();
|
PhysicsEngine physics = live.Physics.Engine;
|
||||||
var projectiles = new ProjectileController(live, physics);
|
var projectiles = new ProjectileController(live);
|
||||||
record.FinalPhysicsState = ProjectileState;
|
record.FinalPhysicsState = ProjectileState;
|
||||||
Assert.True(record.ObjectClock.IsActive);
|
Assert.True(record.ObjectClock.IsActive);
|
||||||
Assert.True(projectiles.TryBind(
|
Assert.True(projectiles.TryBind(
|
||||||
|
|
@ -287,8 +287,8 @@ public sealed class LiveEntityAnimationSchedulerTests
|
||||||
var (live, record, animation) = BuildVisibleAnimatedWithPosFrames(
|
var (live, record, animation) = BuildVisibleAnimatedWithPosFrames(
|
||||||
RemoteGuid,
|
RemoteGuid,
|
||||||
rootOrigin: Vector3.Zero);
|
rootOrigin: Vector3.Zero);
|
||||||
var physics = new PhysicsEngine();
|
PhysicsEngine physics = live.Physics.Engine;
|
||||||
var projectiles = new ProjectileController(live, physics);
|
var projectiles = new ProjectileController(live);
|
||||||
record.FinalPhysicsState = ProjectileState;
|
record.FinalPhysicsState = ProjectileState;
|
||||||
var remote = new AcDream.Runtime.Physics.RemoteMotion
|
var remote = new AcDream.Runtime.Physics.RemoteMotion
|
||||||
{
|
{
|
||||||
|
|
@ -642,7 +642,7 @@ public sealed class LiveEntityAnimationSchedulerTests
|
||||||
new AnimationHookRouter(),
|
new AnimationHookRouter(),
|
||||||
poses))
|
poses))
|
||||||
: new TestAnimationHookCaptureSink(captureHooks);
|
: new TestAnimationHookCaptureSink(captureHooks);
|
||||||
projectiles ??= new ProjectileController(live, physics);
|
projectiles ??= new ProjectileController(live);
|
||||||
return new LiveEntityAnimationScheduler(
|
return new LiveEntityAnimationScheduler(
|
||||||
live,
|
live,
|
||||||
identity,
|
identity,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using System.Reflection;
|
||||||
using AcDream.App.Physics;
|
using AcDream.App.Physics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.World;
|
namespace AcDream.App.Tests.World;
|
||||||
|
|
||||||
|
|
@ -161,6 +162,18 @@ public sealed class GameWindowLiveEntityCompositionTests
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
typeof(LiveWorldOriginState),
|
typeof(LiveWorldOriginState),
|
||||||
Assert.Single(projectileFields, field => field.Name == "_origin").FieldType);
|
Assert.Single(projectileFields, field => field.Name == "_origin").FieldType);
|
||||||
|
Assert.Equal(
|
||||||
|
typeof(RuntimeProjectilePhysicsUpdater),
|
||||||
|
Assert.Single(
|
||||||
|
projectileFields,
|
||||||
|
field => field.Name == "_runtimeUpdater").FieldType);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
projectileFields,
|
||||||
|
field => field.FieldType == typeof(PhysicsEngine)
|
||||||
|
|| field.FieldType == typeof(ProjectilePhysicsStepper)
|
||||||
|
|| field.FieldType.IsGenericType
|
||||||
|
&& field.FieldType.GetGenericTypeDefinition()
|
||||||
|
== typeof(Dictionary<,>));
|
||||||
Assert.DoesNotContain(
|
Assert.DoesNotContain(
|
||||||
projectileFields,
|
projectileFields,
|
||||||
field => typeof(Delegate).IsAssignableFrom(field.FieldType)
|
field => typeof(Delegate).IsAssignableFrom(field.FieldType)
|
||||||
|
|
|
||||||
|
|
@ -422,7 +422,7 @@ public sealed class LiveEntityLifecycleStressTests
|
||||||
randomUnit: () => 0.5,
|
randomUnit: () => 0.5,
|
||||||
canAdvanceOwner: ownerId => _effects?.CanAdvanceOwner(ownerId) ?? true);
|
canAdvanceOwner: ownerId => _effects?.CanAdvanceOwner(ownerId) ?? true);
|
||||||
|
|
||||||
EntityObjects = new RuntimeEntityObjectLifetime();
|
EntityObjects = new RuntimeEntityObjectLifetime(Engine);
|
||||||
Runtime = new LiveEntityRuntime(
|
Runtime = new LiveEntityRuntime(
|
||||||
Spatial,
|
Spatial,
|
||||||
new DelegateLiveEntityResourceLifecycle(
|
new DelegateLiveEntityResourceLifecycle(
|
||||||
|
|
@ -463,7 +463,7 @@ public sealed class LiveEntityLifecycleStressTests
|
||||||
Poses);
|
Poses);
|
||||||
_effects = Effects;
|
_effects = Effects;
|
||||||
Router.Register(Effects);
|
Router.Register(Effects);
|
||||||
Projectiles = new ProjectileController(Runtime, Engine);
|
Projectiles = new ProjectileController(Runtime);
|
||||||
LiveLights = new LiveEntityLightController(
|
LiveLights = new LiveEntityLightController(
|
||||||
Runtime,
|
Runtime,
|
||||||
Poses,
|
Poses,
|
||||||
|
|
|
||||||
|
|
@ -199,10 +199,10 @@ public sealed class LiveEntityProjectionWithdrawalControllerTests
|
||||||
Live = LiveEntityRuntimeFixture.Create(
|
Live = LiveEntityRuntimeFixture.Create(
|
||||||
Spatial,
|
Spatial,
|
||||||
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }),
|
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }),
|
||||||
lifecycle);
|
lifecycle,
|
||||||
Projectiles = new ProjectileController(
|
|
||||||
Live,
|
|
||||||
Physics);
|
Physics);
|
||||||
|
Projectiles = new ProjectileController(
|
||||||
|
Live);
|
||||||
Controller = new LiveEntityProjectionWithdrawalController(
|
Controller = new LiveEntityProjectionWithdrawalController(
|
||||||
Live,
|
Live,
|
||||||
Projectiles,
|
Projectiles,
|
||||||
|
|
|
||||||
|
|
@ -53,11 +53,6 @@ public sealed class LiveEntityRuntimeTests
|
||||||
public PhysicsBody Body { get; } = new();
|
public PhysicsBody Body { get; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class ProjectileRuntime(PhysicsBody body) : ILiveEntityProjectileRuntime
|
|
||||||
{
|
|
||||||
public PhysicsBody Body { get; } = body;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class FailingRegisterResources : ILiveEntityResourceLifecycle
|
private sealed class FailingRegisterResources : ILiveEntityResourceLifecycle
|
||||||
{
|
{
|
||||||
public int RegisterCount { get; private set; }
|
public int RegisterCount { get; private set; }
|
||||||
|
|
@ -427,10 +422,12 @@ public sealed class LiveEntityRuntimeTests
|
||||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
|
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
|
||||||
var animation = new AnimationRuntime(entity);
|
var animation = new AnimationRuntime(entity);
|
||||||
var remote = new RemoteMotionRuntime();
|
var remote = new RemoteMotionRuntime();
|
||||||
var projectile = new ProjectileRuntime(remote.Body);
|
|
||||||
runtime.SetAnimationRuntime(guid, animation);
|
runtime.SetAnimationRuntime(guid, animation);
|
||||||
runtime.SetRemoteMotionRuntime(guid, remote);
|
runtime.SetRemoteMotionRuntime(guid, remote);
|
||||||
runtime.SetProjectileRuntime(guid, projectile);
|
IRuntimeProjectile projectile = runtime.BindProjectileRuntime(
|
||||||
|
guid,
|
||||||
|
remote.Body,
|
||||||
|
new ProjectileCollisionSphere(Vector3.Zero, 0.25f));
|
||||||
|
|
||||||
Assert.True(runtime.IsCurrentSpatialAnimation(record, animation));
|
Assert.True(runtime.IsCurrentSpatialAnimation(record, animation));
|
||||||
Assert.True(runtime.IsCurrentSpatialRemoteMotion(record, remote));
|
Assert.True(runtime.IsCurrentSpatialRemoteMotion(record, remote));
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,201 @@ public sealed class RuntimePhysicsStateTests
|
||||||
Assert.Equal(0, lifetime.Physics.SpatialRootCount);
|
Assert.Equal(0, lifetime.Physics.SpatialRootCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanonicalRecordAndPhysicsOwnerOwnProjectileComponentAndWorkset()
|
||||||
|
{
|
||||||
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
RuntimeEntityRecord record =
|
||||||
|
lifetime.Entities.AddActive(Spawn(0x70000031u, 1));
|
||||||
|
lifetime.Entities.SetFullCell(record, 0x01010001u, 0x0101FFFFu);
|
||||||
|
lifetime.Entities.SetFinalPhysicsState(
|
||||||
|
record,
|
||||||
|
PhysicsStateFlags.ReportCollisions
|
||||||
|
| PhysicsStateFlags.Missile
|
||||||
|
| PhysicsStateFlags.AlignPath
|
||||||
|
| PhysicsStateFlags.PathClipped);
|
||||||
|
var body = new PhysicsBody
|
||||||
|
{
|
||||||
|
Position = new Vector3(10f, 20f, 5f),
|
||||||
|
Orientation = Quaternion.Identity,
|
||||||
|
InWorld = true,
|
||||||
|
TransientState = TransientStateFlags.Active,
|
||||||
|
};
|
||||||
|
body.SnapToCell(
|
||||||
|
record.FullCellId,
|
||||||
|
body.Position,
|
||||||
|
body.Position);
|
||||||
|
lifetime.Entities.SetPhysicsBody(record, body);
|
||||||
|
var sphere = new ProjectileCollisionSphere(
|
||||||
|
new Vector3(0.1f, 0f, 0f),
|
||||||
|
0.25f);
|
||||||
|
|
||||||
|
IRuntimeProjectile projectile = lifetime.Physics.BindProjectile(
|
||||||
|
record,
|
||||||
|
body,
|
||||||
|
sphere);
|
||||||
|
lifetime.Physics.AcknowledgeSpatialProjection(
|
||||||
|
record,
|
||||||
|
spatial: true);
|
||||||
|
|
||||||
|
Assert.Same(projectile, record.Projectile);
|
||||||
|
Assert.Same(body, projectile.Body);
|
||||||
|
Assert.Equal(sphere, projectile.CollisionSphere);
|
||||||
|
Assert.True(
|
||||||
|
lifetime.Physics.IsSpatialProjectile(record, projectile));
|
||||||
|
Assert.Equal(1, lifetime.Physics.SpatialProjectileCount);
|
||||||
|
Assert.Equal(
|
||||||
|
1,
|
||||||
|
lifetime.Physics.CaptureOwnership().SpatialProjectileCount);
|
||||||
|
|
||||||
|
Assert.Same(
|
||||||
|
projectile,
|
||||||
|
lifetime.Physics.BindProjectile(record, body, sphere));
|
||||||
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
lifetime.Physics.BindProjectile(
|
||||||
|
record,
|
||||||
|
new PhysicsBody(),
|
||||||
|
sphere));
|
||||||
|
|
||||||
|
lifetime.Physics.RemoveSpatialProjection(record);
|
||||||
|
Assert.Equal(0, lifetime.Physics.SpatialProjectileCount);
|
||||||
|
Assert.Same(projectile, record.Projectile);
|
||||||
|
|
||||||
|
Assert.True(lifetime.Physics.ClearProjectile(record));
|
||||||
|
Assert.Null(record.Projectile);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProjectileAuthoritativeMutationInvalidatesSplitPrediction()
|
||||||
|
{
|
||||||
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
RuntimeEntityRecord record =
|
||||||
|
lifetime.Entities.AddActive(Spawn(0x70000032u, 1));
|
||||||
|
lifetime.Entities.SetFullCell(record, 0x01010001u, 0x0101FFFFu);
|
||||||
|
lifetime.Entities.SetFinalPhysicsState(
|
||||||
|
record,
|
||||||
|
PhysicsStateFlags.ReportCollisions
|
||||||
|
| PhysicsStateFlags.Missile
|
||||||
|
| PhysicsStateFlags.AlignPath
|
||||||
|
| PhysicsStateFlags.PathClipped);
|
||||||
|
var body = new PhysicsBody
|
||||||
|
{
|
||||||
|
Position = new Vector3(10f, 20f, 5f),
|
||||||
|
Orientation = Quaternion.Identity,
|
||||||
|
InWorld = true,
|
||||||
|
TransientState = TransientStateFlags.Active,
|
||||||
|
};
|
||||||
|
body.set_velocity(new Vector3(10f, 0f, 0f));
|
||||||
|
body.SnapToCell(
|
||||||
|
record.FullCellId,
|
||||||
|
body.Position,
|
||||||
|
body.Position);
|
||||||
|
lifetime.Entities.SetPhysicsBody(record, body);
|
||||||
|
IRuntimeProjectile projectile = lifetime.Physics.BindProjectile(
|
||||||
|
record,
|
||||||
|
body,
|
||||||
|
new ProjectileCollisionSphere(Vector3.Zero, 0.25f));
|
||||||
|
lifetime.Physics.AcknowledgeSpatialProjection(
|
||||||
|
record,
|
||||||
|
spatial: true);
|
||||||
|
var updater =
|
||||||
|
new RuntimeProjectilePhysicsUpdater(lifetime.Physics);
|
||||||
|
|
||||||
|
Assert.True(updater.TryBegin(
|
||||||
|
record,
|
||||||
|
quantum: 0.05f,
|
||||||
|
record.ObjectClockEpoch,
|
||||||
|
externalOwnerValid: null,
|
||||||
|
out RuntimeProjectilePhysicsCommit commit));
|
||||||
|
|
||||||
|
lifetime.Entities.AdvanceVectorAuthority(record);
|
||||||
|
Vector3 correction = new(7f, 8f, 9f);
|
||||||
|
Assert.True(updater.ApplyAuthoritativeVector(
|
||||||
|
record,
|
||||||
|
record.VectorAuthorityVersion,
|
||||||
|
record.VelocityAuthorityVersion,
|
||||||
|
correction,
|
||||||
|
Vector3.UnitZ,
|
||||||
|
currentTime: 1.0));
|
||||||
|
|
||||||
|
Assert.True(
|
||||||
|
projectile.PredictionAuthorityVersion
|
||||||
|
> commit.PredictionAuthorityVersion);
|
||||||
|
Assert.False(updater.Complete(
|
||||||
|
commit,
|
||||||
|
liveCenterX: 1,
|
||||||
|
liveCenterY: 1,
|
||||||
|
_ => true));
|
||||||
|
Assert.Equal(correction, body.Velocity);
|
||||||
|
Assert.Equal(Vector3.UnitZ, body.Omega);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProjectileQuantumPublishesOneImmutableRuntimeFrame()
|
||||||
|
{
|
||||||
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
RuntimeEntityRecord record =
|
||||||
|
lifetime.Entities.AddActive(Spawn(0x70000033u, 1));
|
||||||
|
lifetime.Entities.SetFullCell(record, 0x01010001u, 0x0101FFFFu);
|
||||||
|
lifetime.Entities.SetFinalPhysicsState(
|
||||||
|
record,
|
||||||
|
PhysicsStateFlags.ReportCollisions
|
||||||
|
| PhysicsStateFlags.Missile
|
||||||
|
| PhysicsStateFlags.PathClipped);
|
||||||
|
var body = new PhysicsBody
|
||||||
|
{
|
||||||
|
Position = new Vector3(10f, 20f, 5f),
|
||||||
|
Orientation = Quaternion.Identity,
|
||||||
|
InWorld = true,
|
||||||
|
TransientState = TransientStateFlags.Active,
|
||||||
|
};
|
||||||
|
body.SnapToCell(
|
||||||
|
record.FullCellId,
|
||||||
|
body.Position,
|
||||||
|
body.Position);
|
||||||
|
lifetime.Entities.SetPhysicsBody(record, body);
|
||||||
|
lifetime.Physics.BindProjectile(
|
||||||
|
record,
|
||||||
|
body,
|
||||||
|
new ProjectileCollisionSphere(Vector3.Zero, 0.25f));
|
||||||
|
lifetime.Physics.AcknowledgeSpatialProjection(
|
||||||
|
record,
|
||||||
|
spatial: true);
|
||||||
|
var updater =
|
||||||
|
new RuntimeProjectilePhysicsUpdater(lifetime.Physics);
|
||||||
|
|
||||||
|
Assert.True(updater.TryBegin(
|
||||||
|
record,
|
||||||
|
quantum: 0.05f,
|
||||||
|
record.ObjectClockEpoch,
|
||||||
|
externalOwnerValid: null,
|
||||||
|
out RuntimeProjectilePhysicsCommit commit));
|
||||||
|
int acknowledgements = 0;
|
||||||
|
RuntimePhysicsFrameSnapshot published = default;
|
||||||
|
|
||||||
|
Assert.True(updater.Complete(
|
||||||
|
commit,
|
||||||
|
liveCenterX: 1,
|
||||||
|
liveCenterY: 1,
|
||||||
|
snapshot =>
|
||||||
|
{
|
||||||
|
published = snapshot;
|
||||||
|
acknowledgements++;
|
||||||
|
return true;
|
||||||
|
}));
|
||||||
|
|
||||||
|
Assert.Equal(1, acknowledgements);
|
||||||
|
Assert.Equal(body.Position, published.Position);
|
||||||
|
Assert.Equal(body.Orientation, published.Orientation);
|
||||||
|
Assert.Equal(record.FullCellId, published.FullCellId);
|
||||||
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
updater.Complete(
|
||||||
|
commit,
|
||||||
|
liveCenterX: 1,
|
||||||
|
liveCenterY: 1,
|
||||||
|
_ => true));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void EqualLocalKeysInConcurrentRuntimesDoNotShareWorksets()
|
public void EqualLocalKeysInConcurrentRuntimesDoNotShareWorksets()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue