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