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? ExternalOwnerValid { get; init; } internal bool Completed { get; set; } } /// /// Presentation-free owner of the projectile slice in retail /// CPhysicsObj::UpdateObjectInternal (0x005156B0). The caller /// preserves retail hook ordering by holding the returned transaction across /// process_hooks; Runtime owns integration, transition, prediction /// versions, canonical cell state, and the physics shadow. /// 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? 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 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? 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? 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 acknowledgeProjection, Func? 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? externalOwnerValid) => _physics.IsSpatialProjectile(record, projectile) && record.ObjectClockEpoch == objectClockEpoch && IsIdentityCurrent( record, projectile, predictionAuthorityVersion, externalOwnerValid); private bool IsIdentityCurrent( RuntimeEntityRecord record, RuntimeProjectile projectile, ulong predictionAuthorityVersion, Func? 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? 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); }