acdream/src/AcDream.Runtime/Physics/RuntimeProjectilePhysicsUpdater.cs
Erik 2aee33569f 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.
2026-07-26 14:17:42 +02:00

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);
}