using System.Numerics; using AcDream.App.Rendering; using AcDream.App.World; using AcDream.Content; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.World; using DatReaderWriter.DBObjs; using RemoteMotion = AcDream.Runtime.Physics.RemoteMotion; namespace AcDream.App.Physics; internal interface IProjectileSetupResolver { Setup? Resolve(uint setupId); } internal sealed class DatProjectileSetupResolver : IProjectileSetupResolver { private readonly IDatReaderWriter _dats; private readonly object _datLock; public DatProjectileSetupResolver(IDatReaderWriter dats, object datLock) { _dats = dats ?? throw new ArgumentNullException(nameof(dats)); _datLock = datLock ?? throw new ArgumentNullException(nameof(datLock)); } public Setup? Resolve(uint setupId) { lock (_datLock) return _dats.Get(setupId); } } /// /// Update-thread presentation adapter for live retail missiles. Runtime owns /// logical identity, the projectile component, prediction versions, the /// canonical body/cell, and the split simulation transaction. This adapter /// prepares the DAT Setup sphere, preserves animation-hook ordering, and /// projects committed frames into and effect poses. /// /// /// Retail anchors are CPhysicsObj::update_object 0x00515D10, /// CPhysicsObj::UpdateObjectInternal 0x005156B0, and /// CPhysicsObj::SetPositionInternal 0x00515330. The pure /// integration/sweep is invoked by Runtime through /// . /// ACE remains authoritative for collision outcomes, damage, effects, and /// deletion; no such events are synthesized here. /// internal sealed class ProjectileController { internal readonly record struct QuantumStep( LiveEntityRecord Record, WorldEntity Entity, RuntimeProjectilePhysicsCommit RuntimeCommit); private readonly LiveEntityRuntime _liveEntities; private readonly RuntimeProjectilePhysicsUpdater _runtimeUpdater; private readonly ShadowObjectRegistry _shadows; private readonly IProjectileSetupResolver? _setupResolver; private readonly IEntityRootPosePublisher? _rootPoses; private readonly LiveWorldOriginState? _origin; private readonly List _spatialProjectileSnapshot = new(); private double _lastFiniteGameTime; internal ProjectileController( LiveEntityRuntime liveEntities, IProjectileSetupResolver? setupResolver = null, IEntityRootPosePublisher? rootPoses = null, LiveWorldOriginState? origin = null) { _liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities)); _runtimeUpdater = new RuntimeProjectilePhysicsUpdater( liveEntities.Physics); _shadows = liveEntities.Physics.Engine.ShadowObjects; _setupResolver = setupResolver; _rootPoses = rootPoses; _origin = origin; _liveEntities.ProjectionVisibilityChanged += OnProjectionVisibilityChanged; } internal Action? DiagnosticSink { get; set; } internal int Count => _liveEntities.Records.Count( static record => record.ProjectileRuntime is not null); internal bool CanAcceptVectorPayload( uint serverGuid, Vector3 velocity, Vector3 angularVelocity) { bool valid = IsFinite(velocity) && IsFinite(angularVelocity); if (!valid) DiagnosticSink?.Invoke( $"Rejected invalid VectorUpdate for live object 0x{serverGuid:X8}."); return valid; } internal bool CanAcceptPositionPayload( uint serverGuid, CreateObject.ServerPosition position, Vector3? velocity) { var origin = new Vector3( position.PositionX, position.PositionY, position.PositionZ); var orientation = new Quaternion( position.RotationX, position.RotationY, position.RotationZ, position.RotationW); bool valid = IsFinite(origin) && PositionFrameValidation.IsValid( position.LandblockId, origin, orientation) && (velocity is null || IsFinite(velocity.Value)); if (!valid) DiagnosticSink?.Invoke( $"Rejected invalid PositionUpdate for live object 0x{serverGuid:X8}."); return valid; } /// /// Installs a projectile body once, after the live record and render /// projection exist. Re-entry returns the same component and never replays /// logical registration or create-time effects. /// internal bool TryBind( LiveEntityRecord record, Setup setup, double currentTime, int liveCenterX = 0, int liveCenterY = 0) { ArgumentNullException.ThrowIfNull(record); ArgumentNullException.ThrowIfNull(setup); if (!_liveEntities.TryGetRecord(record.ServerGuid, out var liveRecord) || !ReferenceEquals(liveRecord, record) || (record.FinalPhysicsState & PhysicsStateFlags.Missile) == 0) return false; if (record.ProjectileRuntime is RuntimeProjectile retained) { retained.Body.State = record.FinalPhysicsState; return true; } if (record.WorldEntity is not { } entity || record.Snapshot.Physics is not { } physics || record.Snapshot.Position is not { } wirePosition) return false; float scale = physics.Scale ?? record.Snapshot.ObjScale ?? 1f; if (!TryGetCollisionSphere(setup, scale, out ProjectileCollisionSphere sphere)) { DiagnosticSink?.Invoke( $"Missile 0x{record.ServerGuid:X8} Setup 0x{record.Snapshot.SetupTableId ?? 0u:X8} " + $"does not have the supported retail one-sphere collision shape."); return false; } if (!double.IsFinite(currentTime)) { DiagnosticSink?.Invoke( $"Missile 0x{record.ServerGuid:X8} has an invalid physics clock."); return false; } _lastFiniteGameTime = currentTime; PhysicsBody body; if (record.PhysicsBody is { } sharedBody) { // Retail owns one CPhysicsObj. If a non-missile incarnation // already created its MovementManager or entered the Physics-Static // animation workset, classification adopts that same body instead // of replacing it or replaying CreateObject vectors. body = sharedBody; // Retail has one CPhysicsObj, whose Position owns both objcell_id // and the cell-local frame. The graphical record's FullCellId is // only the later projection receipt and is legitimately still // zero while a residence-managed Create awaits presentation. // Validate and adopt the canonical body frame itself; successful // classification projects that same cell into the sidecar below. uint currentCellId = body.CellPosition.ObjCellId; Vector3 currentCellLocal = body.CellPosition.Frame.Origin; if (!IsFinite(body.Position) || !IsFinite(body.Velocity) || !IsFinite(body.Omega) || !PositionFrameValidation.IsValid( currentCellId, currentCellLocal, body.Orientation)) { DiagnosticSink?.Invoke( $"Missile 0x{record.ServerGuid:X8} has an invalid current shared physics frame or vector."); return false; } body.State = record.FinalPhysicsState; body.Friction = NormalizeFriction( physics.Friction ?? record.Snapshot.Friction); body.Elasticity = NormalizeElasticity( physics.Elasticity ?? record.Snapshot.Elasticity ?? 0.05f); // The MovementManager can predate missile classification and may // already have advanced beyond the retained CreateObject frame. // Preserve its current velocity, omega, orientation, and Active // state exactly as retail SetState does. Only normalize the // carried cell frame and translate the legacy remote clock onto // the App projectile clock. body.SnapToCell( currentCellId, body.Position, currentCellLocal); body.LastUpdateTime = currentTime; } else { Vector3 velocity = physics.Velocity ?? Vector3.Zero; Vector3 omega = physics.AngularVelocity ?? Vector3.Zero; if (!IsFinite(entity.Position) || !PositionFrameValidation.IsValid( wirePosition.LandblockId, new Vector3( wirePosition.PositionX, wirePosition.PositionY, wirePosition.PositionZ), entity.Rotation) || !IsFinite(velocity) || !IsFinite(omega)) { DiagnosticSink?.Invoke( $"Missile 0x{record.ServerGuid:X8} has an invalid initial physics frame or vector."); return false; } // Claim the incarnation's one CPhysicsObj before Rebucket can // publish visibility callbacks. A callback that also needs a body // must observe and adopt this same instance, never race a private // projectile candidate into Runtime's canonical component slot. body = _liveEntities.GetOrCreatePhysicsBody( record.ServerGuid, _ => { var created = new PhysicsBody { Friction = NormalizeFriction( physics.Friction ?? record.Snapshot.Friction), Elasticity = NormalizeElasticity( physics.Elasticity ?? record.Snapshot.Elasticity ?? 0.05f), Orientation = entity.Rotation, LastUpdateTime = currentTime, }; created.SnapToCell( wirePosition.LandblockId, entity.Position, new Vector3( wirePosition.PositionX, wirePosition.PositionY, wirePosition.PositionZ)); return created; }); } uint canonicalCellId = body.CellPosition.ObjCellId; entity.SetPosition(body.Position); entity.Rotation = body.Orientation; entity.ParentCellId = canonicalCellId; // Classification can run from the projection-visible callback after // Runtime has already installed this exact cell. Re-entering Rebucket // from that callback would supersede the outer projection transaction // merely to write the same bucket again. Only perform a spatial move // when classification is actually changing residence. bool alreadyProjectedInCanonicalCell = record.IsSpatiallyProjected && record.FullCellId == canonicalCellId; if ((!alreadyProjectedInCanonicalCell && !_liveEntities.RebucketLiveEntity( record.ServerGuid, canonicalCellId)) || !_liveEntities.TryGetRecord(record.ServerGuid, out var currentRecord) || !ReferenceEquals(currentRecord, record) || !ReferenceEquals(currentRecord.WorldEntity, entity) || !ReferenceEquals(currentRecord.PhysicsBody, body) || (currentRecord.FinalPhysicsState & PhysicsStateFlags.Missile) == 0) { return false; } // Rebucket publishes projection callbacks synchronously. A callback // can re-enter classification for this same incarnation and install // the one projectile component before this outer call resumes. Adopt // that component; never replace it with a second Runtime that merely // happens to share the canonical CPhysicsObj body. if (currentRecord.ProjectileRuntime is RuntimeProjectile concurrentRuntime) return ReferenceEquals(concurrentRuntime.Body, body); // A newer Position can legally arrive from a projection callback // while this independent State(Missile) classification is in flight. // Both packets must win: install the component around the current // canonical body frame, never the pre-callback cell captured above. canonicalCellId = body.CellPosition.ObjCellId; RuntimeProjectile runtime = (RuntimeProjectile) _liveEntities.BindProjectileRuntime( record.ServerGuid, body, sphere, () => _liveEntities.IsCurrentRecord(record) && ReferenceEquals(record.WorldEntity, entity) && ReferenceEquals(record.PhysicsBody, body)); if (HasVisibleCell(record) && !IsHidden(record)) { ShadowPositionSynchronizer.Sync( _shadows, entity.Id, body.Position, body.Orientation, canonicalCellId, liveCenterX, liveCenterY); } else if (HasVisibleCell(record)) { // Hidden is a retained in-world CPhysicsObj, not leave_world. // Keep Active/identity but remove collision rows and consume the // hidden clock so UnHide cannot replay a time backlog. body.InWorld = true; body.LastUpdateTime = currentTime; _shadows.Suspend(entity.Id); } else { SuspendOutsideWorld(runtime, entity.Id); } _rootPoses?.UpdateRoot(entity); return true; } internal bool ApplyAuthoritativeVector( LiveEntityRecord expectedRecord, Vector3 velocity, Vector3 angularVelocity, double currentTime) => ApplyAuthoritativeVector( expectedRecord, expectedRecord.VectorAuthorityVersion, expectedRecord.VelocityAuthorityVersion, velocity, angularVelocity, currentTime); internal bool ApplyAuthoritativeVector( LiveEntityRecord expectedRecord, ulong expectedVectorAuthorityVersion, ulong expectedVelocityAuthorityVersion, Vector3 velocity, Vector3 angularVelocity, double currentTime) { ArgumentNullException.ThrowIfNull(expectedRecord); uint serverGuid = expectedRecord.ServerGuid; if (!TryGetCurrent(serverGuid, out LiveEntityRecord record, out RuntimeProjectile runtime)) return false; if (!ReferenceEquals(record, expectedRecord) || record.VectorAuthorityVersion != expectedVectorAuthorityVersion || record.VelocityAuthorityVersion != expectedVelocityAuthorityVersion) return false; // Once Missile is cleared, an existing MovementManager owns ordinary // jump/vector side effects (Airborne, gravity, LeaveGround) on this // same body. Let that established funnel consume the packet. A // projectile-only object still needs the unconditional retail // set_velocity/set_omega writes here. if ((record.FinalPhysicsState & PhysicsStateFlags.Missile) == 0 && record.RemoteMotionRuntime is not null) return false; if (!IsFinite(velocity) || !IsFinite(angularVelocity) || !double.IsFinite(currentTime)) { DiagnosticSink?.Invoke( $"Rejected invalid VectorUpdate for missile 0x{serverGuid:X8}."); return true; } _lastFiniteGameTime = currentTime; return _runtimeUpdater.ApplyAuthoritativeVector( record.Canonical, expectedVectorAuthorityVersion, expectedVelocityAuthorityVersion, velocity, angularVelocity, currentTime, () => TryGetCurrent( serverGuid, out LiveEntityRecord current, out RuntimeProjectile currentRuntime) && ReferenceEquals(current, record) && ReferenceEquals(currentRuntime, runtime)); } internal bool ApplyAuthoritativeState( LiveEntityRecord expectedRecord, PhysicsStateFlags state, double currentTime, int liveCenterX, int liveCenterY) => ApplyAuthoritativeState( expectedRecord, expectedRecord.StateAuthorityVersion, state, currentTime, liveCenterX, liveCenterY); internal bool ApplyAuthoritativeState( LiveEntityRecord expectedRecord, ulong expectedStateAuthorityVersion, PhysicsStateFlags state, double currentTime, int liveCenterX, int liveCenterY) { ArgumentNullException.ThrowIfNull(expectedRecord); uint serverGuid = expectedRecord.ServerGuid; if (!_liveEntities.TryGetRecord(serverGuid, out LiveEntityRecord record)) return false; if (!ReferenceEquals(record, expectedRecord) || record.StateAuthorityVersion != expectedStateAuthorityVersion) return false; bool validClock = double.IsFinite(currentTime); double effectiveClock = validClock ? currentTime : _lastFiniteGameTime; if (validClock) _lastFiniteGameTime = currentTime; if (!validClock) DiagnosticSink?.Invoke( $"Ignored invalid State update clock for live object 0x{serverGuid:X8}; state remains authoritative."); if (record.ProjectileRuntime is RuntimeProjectile runtime) { return _runtimeUpdater.ApplyAuthoritativeState( record.Canonical, expectedStateAuthorityVersion, state, effectiveClock, liveCenterX, liveCenterY, () => _liveEntities.TryGetRecord( serverGuid, out LiveEntityRecord current) && ReferenceEquals(current, record) && ReferenceEquals( current.ProjectileRuntime, runtime)); } // SetState applies to the canonical CPhysicsObj independently of // whether this state also makes projectile-component acquisition // possible. Missing DAT/shape data must never leave the existing body // on stale collision flags. if (record.RemoteMotionRuntime is { } remote) remote.Body.State = state; if ((state & PhysicsStateFlags.Missile) == 0 || record.WorldEntity is not { } entity) return false; // A newer State packet can make an already materialized object a // missile. TryBind adopts an existing MovementManager body so the // live record still represents retail's one CPhysicsObj. Setup? setup = _setupResolver?.Resolve(entity.SourceGfxObjOrSetupId); if (setup is null) { DiagnosticSink?.Invoke( $"Cannot activate missile 0x{serverGuid:X8}: Setup 0x{entity.SourceGfxObjOrSetupId:X8} is unavailable."); return true; } TryBind(record, setup, effectiveClock, liveCenterX, liveCenterY); return true; } /// /// Applies a timestamp-gated server correction to the same predicted body. /// The caller supplies both render-world and wire cell-local coordinates so /// streaming origin changes never leak into the canonical cell frame. /// internal bool ApplyAuthoritativePosition( LiveEntityRecord expectedRecord, Vector3 worldPosition, Vector3 cellLocalPosition, Quaternion orientation, Vector3 velocity, uint fullCellId, double currentTime, int liveCenterX, int liveCenterY) => ApplyAuthoritativePosition( expectedRecord, expectedRecord.PositionAuthorityVersion, expectedRecord.VelocityAuthorityVersion, worldPosition, cellLocalPosition, orientation, velocity, fullCellId, currentTime, liveCenterX, liveCenterY); internal bool ApplyAuthoritativePosition( LiveEntityRecord expectedRecord, ulong expectedPositionAuthorityVersion, ulong expectedVelocityAuthorityVersion, Vector3 worldPosition, Vector3 cellLocalPosition, Quaternion orientation, Vector3 velocity, uint fullCellId, double currentTime, int liveCenterX, int liveCenterY) { ArgumentNullException.ThrowIfNull(expectedRecord); uint serverGuid = expectedRecord.ServerGuid; if (!TryGetCurrent(serverGuid, out LiveEntityRecord record, out RuntimeProjectile runtime) || !ReferenceEquals(record, expectedRecord) || record.PositionAuthorityVersion != expectedPositionAuthorityVersion || (record.FinalPhysicsState & PhysicsStateFlags.Missile) == 0 || record.WorldEntity is not { } entity) return false; if (!double.IsFinite(currentTime) || !IsFinite(worldPosition) || !IsFinite(cellLocalPosition) || !IsFinite(velocity) || !PositionFrameValidation.IsValid( fullCellId, cellLocalPosition, orientation)) { DiagnosticSink?.Invoke( $"Rejected invalid PositionUpdate for missile 0x{serverGuid:X8}."); return true; } _lastFiniteGameTime = currentTime; bool ExternalOwnerValid() => TryGetCurrent( serverGuid, out LiveEntityRecord current, out RuntimeProjectile currentRuntime) && ReferenceEquals(current, record) && ReferenceEquals(currentRuntime, runtime) && ReferenceEquals(current.WorldEntity, entity) && current.PositionAuthorityVersion == expectedPositionAuthorityVersion; return _runtimeUpdater.ApplyAuthoritativePosition( record.Canonical, expectedPositionAuthorityVersion, expectedVelocityAuthorityVersion, worldPosition, cellLocalPosition, orientation, velocity, fullCellId, currentTime, liveCenterX, liveCenterY, snapshot => { if (!ExternalOwnerValid()) return false; entity.SetPosition(snapshot.Position); entity.Rotation = snapshot.Orientation; entity.ParentCellId = snapshot.FullCellId; _rootPoses?.UpdateRoot(entity); return ExternalOwnerValid(); }, ExternalOwnerValid); } /// /// Selects the retained body's ordinary-physics owner. Missile /// classification always uses the projectile transition path. If the /// authoritative Missile bit later clears, that same body still owns /// ordinary physics until a RemoteMotion runtime exists to take it over. /// Component lifetime and classification are deliberately separate. /// internal bool HandlesMovement(uint serverGuid) => TryGetCurrent(serverGuid, out LiveEntityRecord record, out _) && ((record.FinalPhysicsState & PhysicsStateFlags.Missile) != 0 || record.RemoteMotionRuntime is null); internal bool TryGetBody(uint serverGuid, out PhysicsBody body) { if (TryGetCurrent(serverGuid, out _, out RuntimeProjectile runtime)) { body = runtime.Body; return true; } body = null!; return false; } internal bool LeaveWorld(LiveEntityRecord record) { ArgumentNullException.ThrowIfNull(record); if (record.ProjectileRuntime is not RuntimeProjectile runtime || record.WorldEntity is not { } entity) return false; SuspendOutsideWorld(runtime, entity.Id); return true; } internal void Tick( double currentTime, int liveCenterX, int liveCenterY, Vector3? playerWorldPosition) { double elapsed = currentTime - _lastFiniteGameTime; Tick( currentTime, double.IsFinite(elapsed) && elapsed > 0.0 ? (float)elapsed : 0f, liveCenterX, liveCenterY, playerWorldPosition); } internal void Tick( double currentTime, float elapsedSeconds, int liveCenterX, int liveCenterY, Vector3? playerWorldPosition) { if (!double.IsFinite(currentTime)) { DiagnosticSink?.Invoke("Rejected non-finite projectile update clock."); return; } _lastFiniteGameTime = currentTime; _liveEntities.CopySpatialProjectileRecordsTo(_spatialProjectileSnapshot); foreach (LiveEntityRecord record in _spatialProjectileSnapshot) { if (record.ProjectileRuntime is not RuntimeProjectile runtime || !_liveEntities.IsCurrentSpatialProjectile(record, runtime) || record.WorldEntity is not { } entity) continue; runtime.Body.State = record.FinalPhysicsState; if (!HasVisibleCell(record)) { SuspendOutsideWorld(runtime, entity.Id); continue; } if (!runtime.Body.InWorld) { runtime.Body.LastUpdateTime = currentTime; runtime.Body.InWorld = true; ShadowPositionSynchronizer.Sync( _shadows, entity.Id, runtime.Body.Position, runtime.Body.Orientation, record.FullCellId, liveCenterX, liveCenterY); _rootPoses?.UpdateRoot(entity); } if (IsHidden(record)) { _shadows.Suspend(entity.Id); // A retained animation or PositionManager owner consumes the // same record clock in LiveEntityAnimationScheduler. A bare // projectile has no such owner, but Hidden still consumes its // ordinary update_time without integrating root physics. if (record.AnimationRuntime is null && record.RemoteMotionRuntime is null) { RetailObjectActivityResult hiddenActivity = RetailObjectActivityGate.Evaluate( record.ObjectClock, runtime.Body, _liveEntities.GetRootObjectClockDisposition(record.ServerGuid) is RetailObjectClockDisposition.Advance, record.HasPartArray, (record.FinalPhysicsState & PhysicsStateFlags.Static) != 0, runtime.Body.Position, playerWorldPosition, elapsedSeconds); if (hiddenActivity is RetailObjectActivityResult.Active) record.ObjectClock.Advance(elapsedSeconds); } continue; } bool isMissile = (record.FinalPhysicsState & PhysicsStateFlags.Missile) != 0; if (!isMissile) { // World entry/exit is independent of Missile classification. // A projectile can enter a pending bucket, lose Missile, then // hydrate as an ordinary live object. Restore its retained // shadow/body membership without reactivating projectile // integration or consuming the out-of-world clock gap. // A retained body without MovementManager still remains an // active CPhysicsObj after Missile is cleared. Continue the // same one-sphere update path; PhysicsEngine reads the final // state and therefore disables missile_ignore automatically. if (record.RemoteMotionRuntime is not null) continue; } // Animated owners are admitted by LiveEntityAnimationScheduler so // their PartArray, projectile physics, hooks, and manager tail all // consume one shared object quantum in retail order. if (record.AnimationRuntime is not null) continue; RetailObjectActivityResult activity = RetailObjectActivityGate.Evaluate( record.ObjectClock, runtime.Body, _liveEntities.GetRootObjectClockDisposition(record.ServerGuid) is RetailObjectClockDisposition.Advance, record.HasPartArray, (record.FinalPhysicsState & PhysicsStateFlags.Static) != 0, runtime.Body.Position, playerWorldPosition, elapsedSeconds); if (activity is not RetailObjectActivityResult.Active) continue; RetailObjectQuantumBatch batch = record.ObjectClock.Advance(elapsedSeconds); for (int qi = 0; qi < batch.Count; qi++) { if (!AdvanceQuantum( record, batch.GetQuantum(qi), liveCenterX, liveCenterY)) break; if (record.RemoteMotionRuntime is RemoteMotion remote) { RetailObjectManagerTail.Run( remote.Host?.TargetManager, remote.Movement, partArray: null, remote.Host?.PositionManager); } } } } /// /// Runs the projectile slice of one object quantum already admitted by /// . Animated projectiles call /// this between PartArray advancement and hook/manager processing. /// internal bool AdvanceQuantum( LiveEntityRecord record, float quantum, int liveCenterX, int liveCenterY) { if (!TryBeginQuantum(record, quantum, out QuantumStep step)) return false; return CompleteQuantum(step, liveCenterX, liveCenterY); } internal bool TryBeginQuantum( LiveEntityRecord record, float quantum, out QuantumStep step) { ArgumentNullException.ThrowIfNull(record); step = default; if (!TryGetCurrent( record.ServerGuid, out LiveEntityRecord current, out RuntimeProjectile runtime) || !ReferenceEquals(record, current) || current.WorldEntity is not { } entity || IsHidden(current) || !HasVisibleCell(current)) return false; bool ExternalOwnerValid() => _liveEntities.IsCurrentRecord(current) && ReferenceEquals(current.WorldEntity, entity) && ReferenceEquals(current.ProjectileRuntime, runtime); if (!_runtimeUpdater.TryBegin( current.Canonical, quantum, current.ObjectClockEpoch, ExternalOwnerValid, out RuntimeProjectilePhysicsCommit runtimeCommit)) { return false; } step = new QuantumStep( current, entity, runtimeCommit); return true; } internal bool CompleteQuantum( in QuantumStep step, int liveCenterX, int liveCenterY) { LiveEntityRecord current = step.Record; WorldEntity entity = step.Entity; if (!IsCurrentQuantumIdentity(step)) return false; QuantumStep exactStep = step; return _runtimeUpdater.Complete( exactStep.RuntimeCommit, liveCenterX, liveCenterY, snapshot => { if (!IsCurrentQuantumIdentity(exactStep)) return false; entity.SetPosition(snapshot.Position); entity.Rotation = snapshot.Orientation; entity.ParentCellId = snapshot.FullCellId; if (HasVisibleCell(current)) _rootPoses?.UpdateRoot(entity); return IsCurrentQuantumIdentity(exactStep); }); } private bool IsCurrentQuantumIdentity(in QuantumStep step) => TryGetCurrent( step.Record.ServerGuid, out LiveEntityRecord current, out RuntimeProjectile runtime) && ReferenceEquals(current, step.Record) && ReferenceEquals(current.WorldEntity, step.Entity) && ReferenceEquals(runtime, step.RuntimeCommit.Projectile) && runtime.PredictionAuthorityVersion == step.RuntimeCommit.PredictionAuthorityVersion; private void OnProjectionVisibilityChanged(LiveEntityRecord record, bool visible) { if (record.WorldEntity is not { } entity || !_liveEntities.TryGetRecord(record.ServerGuid, out LiveEntityRecord current) || !ReferenceEquals(current, record)) { return; } if (visible && record.ProjectileRuntime is null && record.PhysicsBody is not null && (record.FinalPhysicsState & PhysicsStateFlags.Missile) != 0) { // A residence-managed Create builds its one CPhysicsObj before // presentation, but Runtime intentionally withholds the committed // cell frame until enter_world completes. Materialization may // therefore observe the body while it is still cell-less and its // eager TryBind correctly refuses that incomplete frame. The // projection-visible edge is the first point at which both the // canonical body and its authoritative placement are guaranteed // to be committed, so retry classification here instead of // fabricating a CreateObject-frame fallback. Setup? setup = _setupResolver?.Resolve( entity.SourceGfxObjOrSetupId); if (setup is not null) { int liveCenterX = _origin?.CenterX ?? 0; int liveCenterY = _origin?.CenterY ?? 0; _ = TryBind( record, setup, _lastFiniteGameTime, liveCenterX, liveCenterY); } } if (record.ProjectileRuntime is not RuntimeProjectile runtime || !ReferenceEquals(current.ProjectileRuntime, runtime)) { return; } if (visible) { // prepare_to_enter_world belongs to the authoritative projection // edge, not to a legacy frame scanner. LiveEntityRuntime has // already rebased the canonical object clock and Active bit before // publishing this notification; restore the remaining CPhysicsObj // residency bit so the first admitted scheduler quantum can run. runtime.Body.State = record.FinalPhysicsState; runtime.Body.InWorld = true; if (IsHidden(record)) { _shadows.Suspend(entity.Id); return; } // add_object_to_cell installs the object's collision shadow as // part of prepare_to_enter_world. Do this on the residency edge, // not lazily on the next simulated quantum: a zero-velocity or // newly ordinary retained projectile may have no later projectile // step from which to repair its shadow membership. int liveCenterX = _origin?.CenterX ?? 0; int liveCenterY = _origin?.CenterY ?? 0; ShadowPositionSynchronizer.Sync( _shadows, entity.Id, runtime.Body.Position, runtime.Body.Orientation, record.FullCellId, liveCenterX, liveCenterY); _rootPoses?.UpdateRoot(entity); return; } // Pending/cell-less records retain their logical projectile runtime, // but retail exit_world removes them from active physics and collision // immediately. They are absent from the frame workset after this edge, // so suspension belongs on the authoritative residency transition. SuspendOutsideWorld(runtime, entity.Id); } private static bool HasVisibleCell(LiveEntityRecord record) => record.IsSpatiallyProjected && record.IsSpatiallyVisible && record.FullCellId != 0; private static bool IsHidden(LiveEntityRecord record) => (record.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0; private void SuspendOutsideWorld(RuntimeProjectile runtime, uint localEntityId) { runtime.Body.InWorld = false; Deactivate(runtime.Body); _shadows.Suspend(localEntityId); } private static void Deactivate(PhysicsBody body) => body.TransientState &= ~TransientStateFlags.Active; private static float NormalizeFriction(float? value) => value is >= 0f and <= 1f && float.IsFinite(value.Value) ? value.Value : PhysicsBody.DefaultFriction; private static float NormalizeElasticity(float value) { if (float.IsNaN(value) || value <= 0f) return 0f; return MathF.Min(value, 0.1f); } private static bool IsFinite(Vector3 value) => float.IsFinite(value.X) && float.IsFinite(value.Y) && float.IsFinite(value.Z); private bool TryGetCurrent( uint serverGuid, out LiveEntityRecord record, out RuntimeProjectile runtime) { if (_liveEntities.TryGetRecord(serverGuid, out record!) && record.ProjectileRuntime is RuntimeProjectile found) { runtime = found; return true; } record = null!; runtime = null!; return false; } private static bool TryGetCollisionSphere( Setup setup, float scale, out ProjectileCollisionSphere sphere) { // Installed DAT audit in 2026-07-13-retail-projectile-vfx-pseudocode.md // pins every required arrow, bolt, and spell projectile to one ordinary // Setup sphere (including Force Bolt's non-centered origin). Do not // fabricate a mesh-derived radius for an unsupported Setup. if (setup.Spheres.Count == 1) { var source = setup.Spheres[0]; sphere = new ProjectileCollisionSphere(source.Origin, source.Radius, scale); if (sphere.IsValid) return true; } sphere = default; return false; } }