From c0a839fa6f2e61ab8570a34a54a670bc059609fc Mon Sep 17 00:00:00 2001 From: SpoinkyNL Date: Sun, 28 Feb 2016 15:36:35 +0100 Subject: [PATCH] Major stability improvements --- Artemis/Artemis/Managers/EffectManager.cs | 47 ++-- Artemis/Artemis/Managers/MainManager.cs | 249 ++++++++++++++++++ .../AudioVisualizer/AudioVisualizerView.xaml | 11 +- .../Effects/Debug/DebugEffectView.xaml | 12 +- .../Games/RocketLeague/RocketLeagueView.xaml | 184 +++++++------ .../RocketLeague/RocketLeagueViewModel.cs | 35 +++ .../Modules/Games/Witcher3/Witcher3Model.cs | 14 +- .../Modules/Games/Witcher3/Witcher3View.xaml | 6 +- .../VolumeDisplay/VolumeDisplayView.xaml | 12 +- .../ViewModels/Abstract/EffectViewModel.cs | 4 +- .../Artemis/ViewModels/SystemTrayViewModel.cs | 3 +- Artemis/Artemis/Views/WelcomeView.xaml | 4 +- 12 files changed, 435 insertions(+), 146 deletions(-) create mode 100644 Artemis/Artemis/Managers/MainManager.cs diff --git a/Artemis/Artemis/Managers/EffectManager.cs b/Artemis/Artemis/Managers/EffectManager.cs index f7187edfb..7bf00886e 100644 --- a/Artemis/Artemis/Managers/EffectManager.cs +++ b/Artemis/Artemis/Managers/EffectManager.cs @@ -23,6 +23,7 @@ namespace Artemis.Managers public List EffectModels { get; set; } public EffectModel ActiveEffect { get; private set; } + public EffectModel PauseEffect { get; private set; } public IEnumerable EnabledOverlays { @@ -75,11 +76,33 @@ namespace Artemis.Managers return; } - // If it's not running, change the effect and start it afterwards. - ActiveEffect = effectModel; + // If it's not running start it, and let the next recursion handle changing the effect + _mainManager.Start(effectModel); + } + + private void ChangeEffectWithPause(EffectModel effectModel) + { + if (PauseEffect != null) + return; + + PauseEffect = effectModel; + _mainManager.Pause(); + _mainManager.PauseCallback += MainManagerOnPauseCallback; + } + + private void MainManagerOnPauseCallback() + { + // Change effect logic + ActiveEffect?.Dispose(); + + ActiveEffect = PauseEffect; ActiveEffect.Enable(); - _mainManager.Start(effectModel); + // Let the ViewModels know + _events.PublishOnUIThread(new ActiveEffectChanged(ActiveEffect.Name)); + + PauseEffect = null; + _mainManager.Unpause(); if (ActiveEffect is GameModel) return; @@ -87,20 +110,6 @@ namespace Artemis.Managers // Non-game effects are stored as the new LastEffect. General.Default.LastEffect = ActiveEffect.Name; General.Default.Save(); - - // Let the ViewModels know - _events.PublishOnUIThread(new ActiveEffectChanged(ActiveEffect.Name)); - } - - private void ChangeEffectWithPause(EffectModel effectModel) - { - _mainManager.Pause(effectModel); - _mainManager.PauseCallback += MainManagerOnPauseCallback; - } - - private void MainManagerOnPauseCallback(EffectModel callbackEffect) - { - } /// @@ -108,6 +117,10 @@ namespace Artemis.Managers /// public void ClearEffect() { + // Don't mess with the ActiveEffect if in the process of changing the effect. + if (PauseEffect != null) + return; + if (ActiveEffect == null) return; diff --git a/Artemis/Artemis/Managers/MainManager.cs b/Artemis/Artemis/Managers/MainManager.cs new file mode 100644 index 000000000..abd890db5 --- /dev/null +++ b/Artemis/Artemis/Managers/MainManager.cs @@ -0,0 +1,249 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using Artemis.Events; +using Artemis.Models; +using Artemis.Utilities.GameState; +using Artemis.Utilities.Keyboard; +using Caliburn.Micro; + +namespace Artemis.Managers +{ + public class MainManager + { + public delegate void PauseCallbackHandler(); + + private readonly int _fps; + private readonly BackgroundWorker _processWorker; + private bool _paused; + + public MainManager(IEventAggregator events) + { + Events = events; + + KeyboardManager = new KeyboardManager(this); + EffectManager = new EffectManager(this, Events); + KeyboardHook = new KeyboardHook(); + + _fps = 25; + UpdateWorker = new BackgroundWorker {WorkerSupportsCancellation = true}; + _processWorker = new BackgroundWorker(); + UpdateWorker.DoWork += UpdateWorker_DoWork; + _processWorker.DoWork += ProcessWorker_DoWork; + + // Process worker will always run (and just do nothing when ProgramEnabled is false) + _processWorker.RunWorkerAsync(); + + ProgramEnabled = false; + Running = false; + + // Create and start the web server + GameStateWebServer = new GameStateWebServer(); + GameStateWebServer.Start(); + } + + public BackgroundWorker UpdateWorker { get; set; } + + public KeyboardManager KeyboardManager { get; set; } + public EffectManager EffectManager { get; set; } + + public KeyboardHook KeyboardHook { get; set; } + + public GameStateWebServer GameStateWebServer { get; set; } + public IEventAggregator Events { get; set; } + + public bool ProgramEnabled { get; private set; } + public bool Suspended { get; set; } + + public bool Running { get; private set; } + public event PauseCallbackHandler PauseCallback; + + /// + /// Take control of the keyboard and start sending data to it + /// + /// Whether starting was successful or not + public bool Start(EffectModel effect = null) + { + // Can't take control when not enabled + if (!ProgramEnabled || UpdateWorker.CancellationPending || UpdateWorker.IsBusy || _paused) + return false; + + // Do nothing if already running + if (Running) + return true; + + // Only continue if a keyboard was loaded + if (!KeyboardManager.LoadLastKeyboard()) + return false; + + Running = true; + if (effect != null) + EffectManager.ChangeEffect(effect); + + // Start the update worker + if (!UpdateWorker.IsBusy) + UpdateWorker.RunWorkerAsync(); + + return Running; + } + + /// + /// Releases control of the keyboard and stop sending data to it + /// + public void Stop() + { + if (!Running || UpdateWorker.CancellationPending || _paused) + return; + + // Stop the update worker + UpdateWorker.CancelAsync(); + UpdateWorker.RunWorkerCompleted += FinishStop; + } + + private void FinishStop(object sender, RunWorkerCompletedEventArgs e) + { + KeyboardManager.ReleaseActiveKeyboard(); + Running = false; + } + + public void Pause() + { + if (!Running || UpdateWorker.CancellationPending || _paused) + return; + + _paused = true; + } + + public void Unpause() + { + if (!_paused) + return; + + _paused = false; + PauseCallback = null; + } + + /// + /// Loads the last active effect and starts the program + /// + public void EnableProgram() + { + ProgramEnabled = true; + Start(EffectManager.GetLastEffect()); + Events.PublishOnUIThread(new ToggleEnabled(ProgramEnabled)); + } + + /// + /// Stops the program + /// + public void DisableProgram() + { + Stop(); + ProgramEnabled = false; + Events.PublishOnUIThread(new ToggleEnabled(ProgramEnabled)); + } + + #region Workers + + private void UpdateWorker_DoWork(object sender, DoWorkEventArgs e) + { + var sw = new Stopwatch(); + while (!UpdateWorker.CancellationPending) + { + // Skip frame when paused + if (_paused) + { + PauseCallback?.Invoke(); + Thread.Sleep(1000 / _fps); + continue; + } + + // Stop if no keyboard/effect are present + if (KeyboardManager.ActiveKeyboard == null || EffectManager.ActiveEffect == null) + { + Thread.Sleep(1000/_fps); + Stop(); + continue; + } + + // Don't stop when the effect is still initialized, just skip this frame + if (!EffectManager.ActiveEffect.Initialized) + { + Thread.Sleep(1000/_fps); + continue; + } + + sw.Start(); + + // Update the current effect + if (EffectManager.ActiveEffect.Initialized) + EffectManager.ActiveEffect.Update(); + + // Get ActiveEffect's bitmap + var bitmap = EffectManager.ActiveEffect.Initialized + ? EffectManager.ActiveEffect.GenerateBitmap() + : null; + + // Draw enabled overlays on top + foreach (var overlayModel in EffectManager.EnabledOverlays) + { + overlayModel.Update(); + bitmap = bitmap != null ? overlayModel.GenerateBitmap(bitmap) : overlayModel.GenerateBitmap(); + } + + // If it exists, send bitmap to the device + if (bitmap != null && KeyboardManager.ActiveKeyboard != null) + { + KeyboardManager.ActiveKeyboard.DrawBitmap(bitmap); + + // debugging TODO: Disable when window isn't shown + Events.PublishOnUIThread(new ChangeBitmap(bitmap)); + } + + // Sleep according to time left this frame + var sleep = (int) (1000/_fps - sw.ElapsedMilliseconds); + if (sleep > 0) + Thread.Sleep(sleep); + sw.Reset(); + } + } + + private void ProcessWorker_DoWork(object sender, DoWorkEventArgs e) + { + while (true) + { + if (!ProgramEnabled) + { + Thread.Sleep(1000); + continue; + } + + var runningProcesses = Process.GetProcesses(); + + // If the currently active effect is a disabled game, get rid of it. + if (EffectManager.ActiveEffect != null) + EffectManager.DisableInactiveGame(); + + // If the currently active effect is a no longer running game, get rid of it. + var activeGame = EffectManager.ActiveEffect as GameModel; + if (activeGame != null) + if (runningProcesses.All(p => p.ProcessName != activeGame.ProcessName)) + EffectManager.DisableGame(activeGame); + + // Look for running games, stopping on the first one that's found. + var newGame = EffectManager.EnabledGames + .FirstOrDefault(g => runningProcesses.Any(p => p.ProcessName == g.ProcessName)); + + // If it's not already enabled, do so. + if (newGame != null && EffectManager.ActiveEffect != newGame) + EffectManager.ChangeEffect(newGame); + + Thread.Sleep(1000); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/Artemis/Artemis/Modules/Effects/AudioVisualizer/AudioVisualizerView.xaml b/Artemis/Artemis/Modules/Effects/AudioVisualizer/AudioVisualizerView.xaml index 4d93659a6..a5a22d8b9 100644 --- a/Artemis/Artemis/Modules/Effects/AudioVisualizer/AudioVisualizerView.xaml +++ b/Artemis/Artemis/Modules/Effects/AudioVisualizer/AudioVisualizerView.xaml @@ -37,15 +37,8 @@