using System.Numerics; using AcDream.Core.Vfx; namespace AcDream.App.Rendering.Vfx; internal interface IWorldSceneParticleVisibility { void MarkVisibleCells(HashSet cellIds); void CompleteFrame(); void AbortFrame(); } /// /// Bridges the retained retail PView result into the next physics update's /// CObjCell::IsInView particle gate. The controller owns only immutable /// frame meaning: one completed viewer position plus the AC cells admitted by /// that completed view. It neither creates emitters nor performs rendering. /// public sealed class ParticleVisibilityController : IWorldSceneParticleVisibility { public const float ExtendedRangeMultiplier = 2f; private readonly HashSet _buildingCellIds = new(); private readonly HashSet _completedCellIds = new(); private Vector3 _buildingViewerPosition; private Vector3 _completedViewerPosition; private bool _frameOpen; private bool _frameUsesWorldView; private bool _hasCompletedWorldView; public void BeginFrame(Vector3 viewerPosition) { _buildingCellIds.Clear(); _buildingViewerPosition = viewerPosition; _frameUsesWorldView = false; _frameOpen = true; } /// /// Declares that this frame has an authoritative world-visibility product. /// That product can come from the unified retail PView or from the outdoor /// landscape fallback. Login and portal-space frames deliberately omit it; /// dedicated pass and examination emitters carry explicit bypass policies. /// public void UseWorldView() { if (_frameOpen) _frameUsesWorldView = true; } public void MarkVisibleCells(HashSet cellIds) { ArgumentNullException.ThrowIfNull(cellIds); if (!_frameOpen || !_frameUsesWorldView) return; _buildingCellIds.UnionWith(cellIds); } public void CompleteFrame() { if (!_frameOpen) return; _frameOpen = false; if (!_frameUsesWorldView) { _completedCellIds.Clear(); _completedViewerPosition = _buildingViewerPosition; _hasCompletedWorldView = false; return; } _completedCellIds.Clear(); _completedCellIds.UnionWith(_buildingCellIds); _completedViewerPosition = _buildingViewerPosition; _hasCompletedWorldView = true; } /// Discards the in-progress visibility product while preserving /// the last completed view consumed by the update thread. public void AbortFrame() { _buildingCellIds.Clear(); _frameUsesWorldView = false; _frameOpen = false; } public void Apply(ParticleSystem particles, float rangeMultiplier) { ArgumentNullException.ThrowIfNull(particles); particles.ApplyRetailView( _completedViewerPosition, _completedCellIds, _hasCompletedWorldView, rangeMultiplier); } public void Reset() { _buildingCellIds.Clear(); _completedCellIds.Clear(); _frameOpen = false; _frameUsesWorldView = false; _hasCompletedWorldView = false; _buildingViewerPosition = default; _completedViewerPosition = default; } }