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.
500 lines
16 KiB
C#
500 lines
16 KiB
C#
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);
|
|
}
|