using AcDream.App.Physics; using AcDream.App.Rendering.Vfx; using AcDream.Core.Physics; using AcDream.Core.Plugins; namespace AcDream.App.World; /// /// Owns retail CPhysicsObj::leave_world's App projection tail for a /// still-live exact record. Component projection is removed before canonical /// runtime withdrawal, matching the shipped failure/retry boundary. /// internal sealed class LiveEntityProjectionWithdrawalController { private readonly LiveEntityRuntime _runtime; private readonly ProjectileController _projectiles; private readonly WorldGameState _worldState; private readonly WorldEvents _worldEvents; private readonly ShadowObjectRegistry _shadows; private readonly EntityEffectPoseRegistry _effectPoses; private readonly LocalPlayerShadowState _localPlayerShadow; public LiveEntityProjectionWithdrawalController( LiveEntityRuntime runtime, ProjectileController projectiles, WorldGameState worldState, WorldEvents worldEvents, ShadowObjectRegistry shadows, EntityEffectPoseRegistry effectPoses, LocalPlayerShadowState localPlayerShadow) { _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); _projectiles = projectiles ?? throw new ArgumentNullException(nameof(projectiles)); _worldState = worldState ?? throw new ArgumentNullException(nameof(worldState)); _worldEvents = worldEvents ?? throw new ArgumentNullException(nameof(worldEvents)); _shadows = shadows ?? throw new ArgumentNullException(nameof(shadows)); _effectPoses = effectPoses ?? throw new ArgumentNullException(nameof(effectPoses)); _localPlayerShadow = localPlayerShadow ?? throw new ArgumentNullException(nameof(localPlayerShadow)); } public bool Withdraw(uint serverGuid, uint localPlayerGuid) { if (!_runtime.TryGetRecord(serverGuid, out LiveEntityRecord record) || record.WorldEntity is null) { return false; } return Withdraw(record, localPlayerGuid); } /// /// Withdraws only the captured incarnation and projection operation. A /// leave-world observer may accept a fresh same-generation Position and /// reproject this record; that newer projection must survive the displaced /// outer operation just as a newer INSTANCE_TS must. /// public bool Withdraw(LiveEntityRecord record, uint localPlayerGuid) { ArgumentNullException.ThrowIfNull(record); if (!_runtime.IsCurrentRecord(record) || record.WorldEntity is null) return false; return Withdraw( record, record.PositionAuthorityVersion, record.ProjectionMutationVersion, localPlayerGuid); } public bool Withdraw( LiveEntityRecord record, ulong positionAuthorityVersion, ulong projectionMutationVersion, uint localPlayerGuid) { ExactProjectionWithdrawalOutcome outcome = WithdrawExact( record, positionAuthorityVersion, projectionMutationVersion, localPlayerGuid); if (outcome.Failure is not null) throw outcome.Failure; return outcome.Disposition is ExactProjectionWithdrawalDisposition.Completed; } public ExactProjectionWithdrawalOutcome WithdrawExact( LiveEntityRecord record, ulong positionAuthorityVersion, ulong projectionMutationVersion, uint localPlayerGuid) { ArgumentNullException.ThrowIfNull(record); if (!_runtime.IsCurrentRecord(record) || record.WorldEntity is null || record.PositionAuthorityVersion != positionAuthorityVersion || record.ProjectionMutationVersion != projectionMutationVersion) { return new( ExactProjectionWithdrawalDisposition.Superseded, Failure: null); } try { LeaveWorld(record, localPlayerGuid); bool completed = _runtime.WithdrawLiveEntityProjection( record, positionAuthorityVersion, projectionMutationVersion); return new( completed ? ExactProjectionWithdrawalDisposition.Completed : ExactProjectionWithdrawalDisposition.Superseded, Failure: null); } catch (Exception error) { bool current = _runtime.IsCurrentRecord(record); ExactProjectionWithdrawalDisposition disposition = current && record.PositionAuthorityVersion == positionAuthorityVersion && record.ProjectionMutationVersion == projectionMutationVersion && record.IsSpatiallyProjected ? ExactProjectionWithdrawalDisposition.Pending : current && record.PositionAuthorityVersion == positionAuthorityVersion && !record.IsSpatiallyProjected ? ExactProjectionWithdrawalDisposition.Completed : ExactProjectionWithdrawalDisposition.Superseded; return new(disposition, error); } } /// /// Removes only the shorter world-facing component projection. This exact /// overload is also used by logical teardown after the active GUID table /// may already expose a replacement incarnation. /// public void LeaveWorld(LiveEntityRecord record, uint localPlayerGuid) { ArgumentNullException.ThrowIfNull(record); if (record.WorldEntity is not { } entity) return; bool retainedProjectileShadow = _projectiles.LeaveWorld(record); if (!retainedProjectileShadow) _shadows.Suspend(entity.Id); _worldState.RemoveById(entity.Id); _worldEvents.ForgetEntity(entity.Id); if (record.ServerGuid == localPlayerGuid && (!_runtime.TryGetRecord(record.ServerGuid, out LiveEntityRecord current) || ReferenceEquals(current, record))) { _localPlayerShadow.Clear(); } // Pose-attached effects keep logical ownership while cell-less. A // missing pose suppresses stale anchors; re-entry republishes the same // WorldEntity frames without replaying create-time resources. _effectPoses.Remove(entity.Id); } } public enum ExactProjectionWithdrawalDisposition { Completed, Pending, Superseded, } public readonly record struct ExactProjectionWithdrawalOutcome( ExactProjectionWithdrawalDisposition Disposition, Exception? Failure);