using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Threading.Tasks; using Artemis.Core.Events; using Artemis.Core.Exceptions; using Artemis.Core.JsonConverters; using Artemis.Core.Models.Profile; using Artemis.Core.Ninject; using Artemis.Core.Plugins.Abstract; using Artemis.Core.Plugins.Models; using Artemis.Core.Services.Interfaces; using Artemis.Core.Services.Storage.Interfaces; using Artemis.Core.Utilities; using Artemis.Storage; using Newtonsoft.Json; using RGB.NET.Core; using Serilog; using Serilog.Events; using SkiaSharp; using Module = Artemis.Core.Plugins.Abstract.Module; namespace Artemis.Core.Services { /// /// Provides Artemis's core update loop /// public class CoreService : ICoreService { private readonly Stopwatch _frameStopWatch; private readonly ILogger _logger; private readonly PluginSetting _loggingLevel; private readonly IPluginService _pluginService; private readonly IProfileService _profileService; private readonly IRgbService _rgbService; private readonly ISurfaceService _surfaceService; private List _dataModelExpansions; private List _modules; private IntroAnimation _introAnimation; // ReSharper disable once UnusedParameter.Local - Storage migration service is injected early to ensure it runs before anything else internal CoreService(ILogger logger, StorageMigrationService _, ISettingsService settingsService, IPluginService pluginService, IRgbService rgbService, ISurfaceService surfaceService, IProfileService profileService, IModuleService moduleService) { _logger = logger; _pluginService = pluginService; _rgbService = rgbService; _surfaceService = surfaceService; _profileService = profileService; _loggingLevel = settingsService.GetSetting("Core.LoggingLevel", LogEventLevel.Debug); _frameStopWatch = new Stopwatch(); UpdatePluginCache(); ConfigureJsonConvert(); _rgbService.Surface.Updating += SurfaceOnUpdating; _rgbService.Surface.Updated += SurfaceOnUpdated; _loggingLevel.SettingChanged += (sender, args) => ApplyLoggingLevel(); _pluginService.PluginEnabled += (sender, args) => UpdatePluginCache(); _pluginService.PluginDisabled += (sender, args) => UpdatePluginCache(); } public TimeSpan FrameTime { get; private set; } public bool ModuleRenderingDisabled { get; set; } public List StartupArguments { get; set; } public void Dispose() { // Dispose services _pluginService.Dispose(); } public bool IsInitialized { get; set; } public void Initialize() { if (IsInitialized) throw new ArtemisCoreException("Cannot initialize the core as it is already initialized."); var versionAttribute = typeof(CoreService).Assembly.GetCustomAttribute(); _logger.Information("Initializing Artemis Core version {version}", versionAttribute?.InformationalVersion); ApplyLoggingLevel(); // Initialize the services _pluginService.CopyBuiltInPlugins(); _pluginService.LoadPlugins(StartupArguments.Contains("--ignore-plugin-lock")); var surfaceConfig = _surfaceService.ActiveSurface; if (surfaceConfig != null) _logger.Information("Initialized with active surface entity {surfaceConfig}-{guid}", surfaceConfig.Name, surfaceConfig.EntityId); else _logger.Information("Initialized without an active surface entity"); PlayIntroAnimation(); OnInitialized(); } private void PlayIntroAnimation() { FrameRendering += DrawOverlay; _introAnimation = new IntroAnimation(_logger, _profileService, _surfaceService); // Draw a white overlay over the device void DrawOverlay(object sender, FrameRenderingEventArgs args) { if (_introAnimation == null) args.Canvas.Clear(new SKColor(0, 0, 0)); else _introAnimation.Render(args.DeltaTime, args.Canvas, _rgbService.BitmapBrush.Bitmap.Info); } var introLength = _introAnimation.AnimationProfile.GetAllLayers().Max(l => l.TimelineLength); // Stop rendering after the profile finishes (take 1 second extra in case of slow updates) Task.Run(async () => { await Task.Delay(introLength.Add(TimeSpan.FromSeconds(1))); FrameRendering -= DrawOverlay; _introAnimation.AnimationProfile?.Dispose(); _introAnimation = null; }); } public void SetMainWindowHandle(IntPtr handle) { Constants.MainWindowHandle = handle; } protected virtual void OnFrameRendering(FrameRenderingEventArgs e) { FrameRendering?.Invoke(this, e); } protected virtual void OnFrameRendered(FrameRenderedEventArgs e) { FrameRendered?.Invoke(this, e); } private void UpdatePluginCache() { _modules = _pluginService.GetPluginsOfType().Where(p => p.Enabled).ToList(); _dataModelExpansions = _pluginService.GetPluginsOfType().Where(p => p.Enabled).ToList(); } private void ConfigureJsonConvert() { JsonConvert.DefaultSettings = () => new JsonSerializerSettings { Converters = new List {new SKColorConverter()} }; } private void ApplyLoggingLevel() { _logger.Information("Setting logging level to {loggingLevel}", _loggingLevel.Value); LoggerProvider.LoggingLevelSwitch.MinimumLevel = _loggingLevel.Value; } private void SurfaceOnUpdating(UpdatingEventArgs args) { if (_rgbService.IsRenderPaused) return; try { _frameStopWatch.Restart(); lock (_dataModelExpansions) { // Update all active modules foreach (var dataModelExpansion in _dataModelExpansions) dataModelExpansion.Update(args.DeltaTime); } List modules; lock (_modules) { modules = _modules.Where(m => m.IsActivated).OrderByDescending(m => m.PriorityCategory).ThenByDescending(m => m.Priority).ToList(); } // Update all active modules foreach (var module in modules) module.Update(args.DeltaTime); // If there is no ready bitmap brush, skip the frame if (_rgbService.BitmapBrush == null) return; lock (_rgbService.BitmapBrush) { if (_rgbService.BitmapBrush.Bitmap == null) return; // Render all active modules using var canvas = new SKCanvas(_rgbService.BitmapBrush.Bitmap); canvas.Clear(new SKColor(0, 0, 0)); if (!ModuleRenderingDisabled) { foreach (var module in modules) module.Render(args.DeltaTime, _surfaceService.ActiveSurface, canvas, _rgbService.BitmapBrush.Bitmap.Info); } OnFrameRendering(new FrameRenderingEventArgs(modules, canvas, args.DeltaTime, _rgbService.Surface)); } } catch (Exception e) { throw new ArtemisCoreException("Exception during update", e); } finally { _frameStopWatch.Stop(); FrameTime = _frameStopWatch.Elapsed; } } private void SurfaceOnUpdated(UpdatedEventArgs args) { if (_rgbService.IsRenderPaused) return; OnFrameRendered(new FrameRenderedEventArgs(_rgbService.BitmapBrush, _rgbService.Surface)); } #region Events public event EventHandler Initialized; public event EventHandler FrameRendering; public event EventHandler FrameRendered; private void OnInitialized() { IsInitialized = true; Initialized?.Invoke(this, EventArgs.Empty); } #endregion } }