using System; using System.Collections.Generic; using System.Threading; using Cysharp.Threading.Tasks; using UnityEngine; namespace DanroJump.Hardware.Com { /// /// Отдельная система аппаратной подсветки: плата, шляпа и призовая витрина. /// public sealed class ComLightingSystem : IComLightingController, IDisposable { private readonly Func sendToMain; private readonly Func sendToPrizeStand; private readonly ComLightingOptions options; private CancellationTokenSource hatBlinkCancellation; private CancellationTokenSource lightSequenceCancellation; private LightSequenceState pausedLightSequence; private bool hatIsOn; private bool hatBlinkWasPaused; public ComLightingSystem( ComLightingOptions options, Func sendToMain, Func sendToPrizeStand) { this.options = options; this.sendToMain = sendToMain ?? (_ => false); this.sendToPrizeStand = sendToPrizeStand ?? (_ => false); } public bool SetPrizeStandIdleLighting() { return sendToPrizeStand(options.PrizeStandIdleLightingCommand); } public bool HighlightPrizeBox(int boxNumber) { return boxNumber > 0 && sendToPrizeStand($"{options.PrizeStandHighlightBoxCommandPrefix}{boxNumber:D2}"); } public void PlayBoardLight(LightMode mode, float cycles = -1f) { StopBoardLightSequence(); float resolvedCycles = cycles > 0f ? cycles : options.DefaultLightCycles; StartBoardLightSequenceTask(new[] { new LightStep(mode, resolvedCycles) }, false); } public void PlayBoardLightSequence(IReadOnlyList steps, bool loop) { if (steps == null || steps.Count == 0) { return; } StopBoardLightSequence(); StartBoardLightSequenceTask(steps, loop); } public void PlayDefaultBoardLight() { PlayBoardLight(options.DefaultLightMode, options.DefaultLightCycles); } public void StopBoardLightSequence() { CancelAndDispose(ref lightSequenceCancellation); pausedLightSequence = default; } public void StopAllBoardEffects() { StopBoardLightSequence(); sendToMain(options.BoardEffectsOffCommand); } public void BlinkHat() { BlinkHat(options.HatBlinkIntervalSeconds); } public void BlinkHat(float intervalSeconds) { StopHatBlink(false); hatIsOn = true; hatBlinkCancellation = new CancellationTokenSource(); HatBlinkAsync(Mathf.Max(0.05f, intervalSeconds), hatBlinkCancellation).Forget(Debug.LogException); } public void StopHatBlink(bool leaveHatEnabled = true) { CancelAndDispose(ref hatBlinkCancellation); hatIsOn = leaveHatEnabled; sendToMain(leaveHatEnabled ? options.HatOnCommand : options.HatOffCommand); } public void PauseLighting() { if (hatBlinkCancellation != null) { hatBlinkWasPaused = true; CancelAndDispose(ref hatBlinkCancellation); } else { hatBlinkWasPaused = false; } if (lightSequenceCancellation != null) { CancelAndDispose(ref lightSequenceCancellation); pausedLightSequence = new LightSequenceState(true); } } public void ResumeLighting() { if (hatBlinkWasPaused) { hatBlinkWasPaused = false; BlinkHat(options.HatBlinkIntervalSeconds); } else if (hatIsOn) { sendToMain(options.HatOnCommand); } if (pausedLightSequence.HasPausedSequence) { PlayDefaultBoardLight(); } } public void Dispose() { StopBoardLightSequence(); CancelAndDispose(ref hatBlinkCancellation); } private async UniTask HatBlinkAsync(float intervalSeconds, CancellationTokenSource cancellationSource) { var token = cancellationSource.Token; try { while (!token.IsCancellationRequested) { sendToMain(UnityEngine.Random.Range(0f, 1f) < 0.5f ? options.HatOnCommand : options.HatOffCommand); float wait = options.HatBlinkVarianceSeconds > 0f ? Mathf.Max( 0.05f, intervalSeconds + UnityEngine.Random.Range(-options.HatBlinkVarianceSeconds, options.HatBlinkVarianceSeconds)) : intervalSeconds; await UniTask.Delay( TimeSpan.FromSeconds(wait), DelayType.UnscaledDeltaTime, PlayerLoopTiming.Update, token); } } catch (OperationCanceledException) when (token.IsCancellationRequested) { // ignored } finally { ClearCancellationIfCurrent(ref hatBlinkCancellation, cancellationSource); } } private void StartBoardLightSequenceTask(IReadOnlyList steps, bool loop) { lightSequenceCancellation = new CancellationTokenSource(); BoardLightSequenceAsync(steps, loop, lightSequenceCancellation).Forget(Debug.LogException); } private async UniTask BoardLightSequenceAsync( IReadOnlyList steps, bool loop, CancellationTokenSource cancellationSource) { var token = cancellationSource.Token; try { do { foreach (LightStep step in steps) { token.ThrowIfCancellationRequested(); sendToMain(BuildBoardLightCommand(step.Mode, step.Cycles)); float waitSeconds = LightTimings.GetCycleSeconds(step.Mode) * step.Cycles; if (step.Mode == LightMode.Off || waitSeconds <= 0f) { continue; } float elapsed = 0f; while (elapsed < waitSeconds) { float chunk = Mathf.Min(2f, waitSeconds - elapsed); await UniTask.Delay( TimeSpan.FromSeconds(chunk), DelayType.UnscaledDeltaTime, PlayerLoopTiming.Update, token); elapsed += chunk; if (elapsed < waitSeconds) { int remainSeconds = Mathf.CeilToInt(waitSeconds - elapsed); sendToMain(BuildBoardLightCommand(step.Mode, remainSeconds)); } } } } while (loop && !token.IsCancellationRequested); } catch (OperationCanceledException) when (token.IsCancellationRequested) { // ignored } finally { ClearCancellationIfCurrent(ref lightSequenceCancellation, cancellationSource); } } private string BuildBoardLightCommand(LightMode mode, float cycles) { if (mode == LightMode.Off) { return options.BoardEffectsOffCommand; } int seconds = LightTimings.CyclesToCommandSeconds(mode, cycles); return BuildBoardLightCommand(mode, seconds); } private string BuildBoardLightCommand(LightMode mode, int durationSeconds) { if (mode == LightMode.Off) { return options.BoardEffectsOffCommand; } return $"{options.BoardEffectsCommandPrefix}{(int)mode:D2}{Mathf.Clamp(durationSeconds, 0, 99):D2}"; } private static void CancelAndDispose(ref CancellationTokenSource cancellationSource) { if (cancellationSource == null) { return; } var source = cancellationSource; cancellationSource = null; try { source.Cancel(); } catch { // ignored } source.Dispose(); } private static void ClearCancellationIfCurrent( ref CancellationTokenSource currentSource, CancellationTokenSource completedSource) { if (currentSource != completedSource) { return; } currentSource = null; completedSource.Dispose(); } private readonly struct LightSequenceState { public readonly bool HasPausedSequence; public LightSequenceState(bool hasPausedSequence) { HasPausedSequence = hasPausedSequence; } } } }