using System.Numerics; using AcDream.App.Rendering; using AcDream.App.World; using AcDream.Core.Physics; using AcDream.Core.Physics.Motion; using AcDream.Core.World; using AcDream.Runtime.Physics; namespace AcDream.App.Physics; /// /// Graphical adapter for Runtime's presentation-free remote-object physics. /// It supplies DAT-derived shape dimensions and animation callbacks, then /// projects the committed root snapshot into the retained App entity. /// internal sealed class RemotePhysicsUpdater { private readonly RuntimeRemotePhysicsUpdater _runtime; private readonly Func _getSetupCylinder; private readonly Action _applyServerControlledVelocityCycle; private readonly List _spatialRemoteSnapshot = new(); internal RemotePhysicsUpdater( RuntimePhysicsState physics, Func getSetupCylinder, Action applyServerControlledVelocityCycle) { _runtime = new RuntimeRemotePhysicsUpdater( physics ?? throw new ArgumentNullException(nameof(physics))); _getSetupCylinder = getSetupCylinder ?? throw new ArgumentNullException(nameof(getSetupCylinder)); _applyServerControlledVelocityCycle = applyServerControlledVelocityCycle ?? throw new ArgumentNullException( nameof(applyServerControlledVelocityCycle)); } public void TickHiddenEntities( LiveEntityRuntime liveEntities, uint localPlayerServerGuid, float dt, Action publishRootPose, Action? processAnimationHooks = null, Action? markPartPoseDirty = null) { ArgumentNullException.ThrowIfNull(liveEntities); ArgumentNullException.ThrowIfNull(publishRootPose); Vector3? playerPosition = null; if (localPlayerServerGuid != 0 && liveEntities.TryGetWorldEntity( localPlayerServerGuid, out WorldEntity playerEntity)) { playerPosition = playerEntity.Position; } liveEntities.CopySpatialRemoteMotionRecordsTo( _spatialRemoteSnapshot); foreach (LiveEntityRecord record in _spatialRemoteSnapshot) { if (record.ServerGuid == localPlayerServerGuid || (record.FinalPhysicsState & PhysicsStateFlags.Hidden) == 0 || record.RemoteMotionRuntime is not RemoteMotion remote || !liveEntities.IsCurrentSpatialRemoteMotion(record, remote) || record.WorldEntity is not { } entity) { continue; } LiveEntityAnimationState? animation = record.AnimationRuntime as LiveEntityAnimationState; RetailObjectActivityResult activity = RetailObjectActivityGate.Evaluate( record.ObjectClock, remote.Body, liveEntities.GetRootObjectClockDisposition( record.ServerGuid) is RetailObjectClockDisposition.Advance, record.HasPartArray, (record.FinalPhysicsState & PhysicsStateFlags.Static) != 0, entity.Position, playerPosition, dt); if (activity is not RetailObjectActivityResult.Active) continue; AnimationSequencer? sequencer = animation?.Sequencer; ulong objectClockEpoch = record.ObjectClockEpoch; RetailObjectQuantumBatch batch = record.ObjectClock.Advance(dt); for (int qi = 0; qi < batch.Count; qi++) { if (!IsCurrent( liveEntities, record, entity, remote, objectClockEpoch) || !TickHidden( remote, entity, batch.GetQuantum(qi), sequencer?.Manager, processAnimationHooks, sequencer, liveEntities, record, objectClockEpoch)) { break; } } if (batch.Count > 0 && IsCurrent( liveEntities, record, entity, remote, objectClockEpoch)) { markPartPoseDirty?.Invoke(record.ServerGuid); publishRootPose(entity); } } } public bool Tick( RemoteMotion remote, LiveEntityAnimationState animation, float dt, MotionDeltaFrame rootMotionLocalFrame, int liveCenterX, int liveCenterY, Action? processAnimationHooks = null, LiveEntityRuntime? ownerRuntime = null, LiveEntityRecord? ownerRecord = null, ulong ownerClockEpoch = 0) { ArgumentNullException.ThrowIfNull(animation); return Tick( remote, animation.Entity, animation.Scale, animation.Sequencer, animation, dt, rootMotionLocalFrame, liveCenterX, liveCenterY, processAnimationHooks, ownerRuntime, ownerRecord, ownerClockEpoch); } public bool Tick( RemoteMotion remote, WorldEntity entity, float objectScale, AnimationSequencer? sequencer, LiveEntityAnimationState? animationForVelocityCycle, float dt, MotionDeltaFrame rootMotionLocalFrame, int liveCenterX, int liveCenterY, Action? processAnimationHooks = null, LiveEntityRuntime? ownerRuntime = null, LiveEntityRecord? ownerRecord = null, ulong ownerClockEpoch = 0) { ArgumentNullException.ThrowIfNull(remote); ArgumentNullException.ThrowIfNull(entity); if (ownerRuntime is null || ownerRecord is null) { return false; } bool OwnerValid() => IsCurrent( ownerRuntime, ownerRecord, entity, remote, ownerClockEpoch); var (radius, height) = _getSetupCylinder(ownerRecord.ServerGuid, entity); Action? staleCycle = animationForVelocityCycle is null ? null : velocity => _applyServerControlledVelocityCycle( ownerRecord.ServerGuid, animationForVelocityCycle, remote, velocity); return _runtime.Tick( ownerRecord.Canonical, remote, objectScale, sequencer, dt, ownerClockEpoch, rootMotionLocalFrame, radius, height, liveCenterX, liveCenterY, processAnimationHooks, staleCycle, snapshot => { if (!OwnerValid()) return false; entity.SetPosition(snapshot.Position); entity.ParentCellId = snapshot.FullCellId; entity.Rotation = snapshot.Orientation; return OwnerValid(); }, OwnerValid); } public bool TickHidden( RemoteMotion remote, WorldEntity entity, float dt, MotionTableManager? partArrayHandleMovement = null, Action? processAnimationHooks = null, AnimationSequencer? sequencer = null, LiveEntityRuntime? ownerRuntime = null, LiveEntityRecord? ownerRecord = null, ulong ownerClockEpoch = 0) { ArgumentNullException.ThrowIfNull(remote); ArgumentNullException.ThrowIfNull(entity); if (ownerRuntime is null || ownerRecord is null) { return false; } bool OwnerValid() => IsCurrent( ownerRuntime, ownerRecord, entity, remote, ownerClockEpoch); var (radius, height) = _getSetupCylinder(ownerRecord.ServerGuid, entity); return _runtime.TickHidden( ownerRecord.Canonical, remote, dt, ownerClockEpoch, radius, height, partArrayHandleMovement, processAnimationHooks, sequencer, snapshot => { if (!OwnerValid()) return false; entity.SetPosition(snapshot.Position); entity.ParentCellId = snapshot.FullCellId; entity.Rotation = snapshot.Orientation; return OwnerValid(); }, OwnerValid); } public void SyncRemoteShadowToBody( uint entityId, IRuntimeRemotePlacement remote, int liveCenterX, int liveCenterY, uint? authoritativeCellId = null) => _runtime.SyncRemoteShadowToBody( entityId, remote, liveCenterX, liveCenterY, authoritativeCellId); public void SyncRemoteShadowToBody( uint entityId, PhysicsBody body, int liveCenterX, int liveCenterY, uint authoritativeCellId) => _runtime.SyncRemoteShadowToBody( entityId, body, liveCenterX, liveCenterY, authoritativeCellId); internal static bool ShouldSynchronizeShadowPose( Vector3 currentPosition, Quaternion currentOrientation, Vector3 lastPosition, Quaternion lastOrientation) => RuntimeRemotePhysicsUpdater.ShouldSynchronizeShadowPose( currentPosition, currentOrientation, lastPosition, lastOrientation); internal static bool ShouldSynchronizeShadow( bool cellChanged, Vector3 currentPosition, Quaternion currentOrientation, Vector3 lastPosition, Quaternion lastOrientation) => RuntimeRemotePhysicsUpdater.ShouldSynchronizeShadow( cellChanged, currentPosition, currentOrientation, lastPosition, lastOrientation); private static bool IsCurrent( LiveEntityRuntime runtime, LiveEntityRecord record, WorldEntity entity, RemoteMotion remote, ulong objectClockEpoch) => record.ObjectClockEpoch == objectClockEpoch && runtime.IsCurrentSpatialRemoteMotion(record, remote) && ReferenceEquals(record.WorldEntity, entity); }