Ejemplo n.º 1
0
        public static IDisposable HelpWiring(ConfigurationVM config, Button button, TextBlock helpBlock, IObservable <bool>?show = null)
        {
            CompositeDisposable ret = new CompositeDisposable();

            show ??= Observable.Return(true);

            show.Select(x => x ? Visibility.Visible : Visibility.Collapsed)
            .Subscribe(x => button.Visibility = x)
            .DisposeWith(ret);

            Observable.CombineLatest(
                show,
                config.WhenAnyValue(x => x.ShowHelp),
                (newPatcher, on) => on && newPatcher)
            .Select(x => x ? Visibility.Visible : Visibility.Collapsed)
            .Subscribe(x => helpBlock.Visibility = x)
            .DisposeWith(ret);

            config.WhenAnyValue(x => x.ShowHelpToggleCommand)
            .Subscribe(x => button.Command = x)
            .DisposeWith(ret);

            config.WhenAnyValue(x => x.ShowHelp)
            .Select(x => x ? 1.0d : 0.4d)
            .Subscribe(x => button.Opacity = x)
            .DisposeWith(ret);

            return(ret);
        }
Ejemplo n.º 2
0
        public NewProfileVM(ConfigurationVM config, Action <ProfileVM> postRun)
        {
            ReleaseOptions.AddRange(EnumExt.GetValues <GameRelease>());

            this.WhenAnyValue(x => x.SelectedGame)
            .Subscribe(game =>
            {
                if (game == null)
                {
                    return;
                }
                var profile = new ProfileVM(config, game.Value)
                {
                    Nickname = Nickname
                };
                config.Profiles.AddOrUpdate(profile);
                postRun(profile);
            })
            .DisposeWith(this);
        }
Ejemplo n.º 3
0
        public ProfilesDisplayVM(ConfigurationVM parent, ViewModel previousPage)
        {
            _previous     = previousPage;
            Config        = parent;
            GoBackCommand = ReactiveCommand.Create(() =>
            {
                parent.MainVM.ActivePanel = _previous;
            });
            AddCommand = ReactiveCommand.Create(() =>
            {
                DisplayObject = new NewProfileVM(Config, (profile) =>
                {
                    if (string.IsNullOrWhiteSpace(profile.Nickname))
                    {
                        profile.Nickname = profile.Release.ToDescriptionString();
                    }
                });
                DisplayedProfile = null;
            });

            ProfilesDisplay = parent.Profiles.Connect()
                              .Transform(x => new ProfileDisplayVM(this, x))
                              // Select the currently active profile during initial display
                              .OnItemAdded(p =>
            {
                if (DisplayedProfile == null || (p.Profile?.IsActive ?? false))
                {
                    DisplayedProfile = p;
                }
            })
                              .ToObservableCollection(this);

            this.WhenAnyValue(x => x.DisplayedProfile)
            .NotNull()
            .Subscribe(p => DisplayObject = p)
            .DisposeWith(this);
        }
Ejemplo n.º 4
0
        public ProfileVM(ConfigurationVM parent, GameRelease?release = null, string?id = null)
        {
            ID      = id ?? Guid.NewGuid().ToString();
            Config  = parent;
            Release = release ?? GameRelease.Oblivion;
            AddGitPatcherCommand      = ReactiveCommand.Create(() => SetInitializer(new GitPatcherInitVM(this)));
            AddSolutionPatcherCommand = ReactiveCommand.Create(() => SetInitializer(new SolutionPatcherInitVM(this)));
            AddCliPatcherCommand      = ReactiveCommand.Create(() => SetInitializer(new CliPatcherInitVM(this)));
            AddSnippetPatcherCommand  = ReactiveCommand.Create(() => SetPatcherForInitialConfiguration(new CodeSnippetPatcherVM(this)));

            ProfileDirectory = Path.Combine(Execution.Paths.WorkingDirectory, ID);
            WorkingDirectory = Execution.Paths.ProfileWorkingDirectory(ID);

            var dataFolderResult = this.WhenAnyValue(x => x.DataPathOverride)
                                   .Select(path =>
            {
                if (path != null)
                {
                    return(Observable.Return(GetResponse <string> .Succeed(path)));
                }
                Log.Logger.Information("Starting to locate data folder");
                return(this.WhenAnyValue(x => x.Release)
                       .ObserveOn(RxApp.TaskpoolScheduler)
                       .Select(x =>
                {
                    try
                    {
                        if (!GameLocations.TryGetGameFolder(x, out var gameFolder))
                        {
                            return GetResponse <string> .Fail("Could not automatically locate Data folder.  Run Steam/GoG/etc once to properly register things.");
                        }
                        return GetResponse <string> .Succeed(Path.Combine(gameFolder, "Data"));
                    }
                    catch (Exception ex)
                    {
                        return GetResponse <string> .Fail(string.Empty, ex);
                    }
                }));
            })
                                   .Switch()
                                   // Watch folder for existance
                                   .Select(x =>
            {
                if (x.Failed)
                {
                    return(Observable.Return(x));
                }
                return(Noggog.ObservableExt.WatchFile(x.Value)
                       .StartWith(Unit.Default)
                       .Select(_ =>
                {
                    if (Directory.Exists(x.Value))
                    {
                        return x;
                    }
                    return GetResponse <string> .Fail($"Data folder did not exist: {x.Value}");
                }));
            })
                                   .Switch()
                                   .StartWith(GetResponse <string> .Fail("Data folder uninitialized"))
                                   .Replay(1)
                                   .RefCount();

            _DataFolder = dataFolderResult
                          .Select(x => x.Value)
                          .ToGuiProperty <string>(this, nameof(DataFolder), string.Empty);

            dataFolderResult
            .Subscribe(d =>
            {
                if (d.Failed)
                {
                    Log.Logger.Error($"Could not locate data folder: {d.Reason}");
                }
                else
                {
                    Log.Logger.Information($"Data Folder: {d.Value}");
                }
            })
            .DisposeWith(this);

            var loadOrderResult = Observable.CombineLatest(
                this.WhenAnyValue(x => x.Release),
                dataFolderResult,
                (release, dataFolder) => (release, dataFolder))
                                  .ObserveOn(RxApp.TaskpoolScheduler)
                                  .Select(x =>
            {
                if (x.dataFolder.Failed)
                {
                    return(Results: Observable.Empty <IChangeSet <LoadOrderEntryVM> >(), State: Observable.Return(ErrorResponse.Fail("Data folder not set")));
                }
                Log.Logger.Error($"Getting live load order for {x.release} -> {x.dataFolder.Value}");
                var liveLo = Mutagen.Bethesda.LoadOrder.GetLiveLoadOrder(x.release, x.dataFolder.Value, out var errors)
                             .Transform(listing => new LoadOrderEntryVM(listing, x.dataFolder.Value))
                             .DisposeMany();
                return(Results: liveLo, State: errors);
            })
                                  .StartWith((Results: Observable.Empty <IChangeSet <LoadOrderEntryVM> >(), State: Observable.Return(ErrorResponse.Fail("Load order uninitialized"))))
                                  .Replay(1)
                                  .RefCount();

            LoadOrder = loadOrderResult
                        .Select(x => x.Results)
                        .Switch()
                        .AsObservableList();

            loadOrderResult.Select(lo => lo.State)
            .Switch()
            .Subscribe(loErr =>
            {
                if (loErr.Succeeded)
                {
                    Log.Logger.Information($"Load order location successful");
                }
                else
                {
                    Log.Logger.Information($"Load order location error: {loErr.Reason}");
                }
            })
            .DisposeWith(this);

            _LargeOverallError = Observable.CombineLatest(
                dataFolderResult,
                loadOrderResult
                .Select(x => x.State)
                .Switch(),
                Patchers.Connect()
                .ObserveOnGui()
                .FilterOnObservable(p => p.WhenAnyValue(x => x.IsOn), scheduler: RxApp.MainThreadScheduler)
                .QueryWhenChanged(q => q)
                .StartWith(Noggog.ListExt.Empty <PatcherVM>()),
                Patchers.Connect()
                .ObserveOnGui()
                .FilterOnObservable(p => Observable.CombineLatest(
                                        p.WhenAnyValue(x => x.IsOn),
                                        p.WhenAnyValue(x => x.State.IsHaltingError),
                                        (on, halting) => on && halting),
                                    scheduler: RxApp.MainThreadScheduler)
                .QueryWhenChanged(q => q)
                .StartWith(Noggog.ListExt.Empty <PatcherVM>()),
                LoadOrder.Connect()
                .ObserveOnGui()
                .FilterOnObservable(
                    x => x.WhenAnyValue(y => y.Exists)
                    .DistinctUntilChanged()
                    .Select(x => !x),
                    scheduler: RxApp.MainThreadScheduler)
                .QueryWhenChanged(q => q)
                .StartWith(Noggog.ListExt.Empty <LoadOrderEntryVM>())
                .Throttle(TimeSpan.FromMilliseconds(200), RxApp.MainThreadScheduler),
                (dataFolder, loadOrder, enabledPatchers, erroredEnabledPatchers, missingMods) =>
            {
                if (enabledPatchers.Count == 0)
                {
                    return(GetResponse <PatcherVM> .Fail("There are no enabled patchers to run."));
                }
                if (!dataFolder.Succeeded)
                {
                    return(dataFolder.BubbleFailure <PatcherVM>());
                }
                if (!loadOrder.Succeeded)
                {
                    return(loadOrder.BubbleFailure <PatcherVM>());
                }
                if (missingMods.Count > 0)
                {
                    return(GetResponse <PatcherVM> .Fail($"Load order had mods that were missing:{Environment.NewLine}{string.Join(Environment.NewLine, missingMods.Select(x => x.Listing.ModKey))}"));
                }
                if (erroredEnabledPatchers.Count > 0)
                {
                    var errPatcher = erroredEnabledPatchers.First();
                    return(GetResponse <PatcherVM> .Fail(errPatcher, $"\"{errPatcher.DisplayName}\" has a blocking error: {errPatcher.State.RunnableState.Reason}"));
                }
                return(GetResponse <PatcherVM> .Succeed(null !));
            })
Ejemplo n.º 5
0
        public MainVM(Window window)
        {
            _window = window;
            var dotNet = Observable.Interval(TimeSpan.FromSeconds(10), RxApp.TaskpoolScheduler)
                         .StartWith(0)
                         .SelectTask(async i =>
            {
                try
                {
                    var ret = await DotNetCommands.DotNetSdkVersion(CancellationToken.None);
                    Log.Logger.Information($"dotnet SDK: {ret}");
                    return(ret);
                }
                catch (Exception ex)
                {
                    Log.Logger.Error(ex, $"Error retrieving dotnet SDK version");
                    return(default(Version?));
                }
            });

            DotNetSdkInstalled = dotNet
                                 .Take(1)
                                 .Merge(dotNet
                                        .FirstAsync(v => v != null))
                                 .DistinctUntilChanged()
                                 .Replay(1)
                                 .RefCount();

            Configuration        = new ConfigurationVM(this);
            ActivePanel          = Configuration;
            DiscardActionCommand = NoggogCommand.CreateFromObject(
                objectSource: this.WhenAnyValue(x => x.TargetConfirmation),
                canExecute: target => target != null,
                execute: (_) =>
            {
                TargetConfirmation = null;
            },
                disposable: this.CompositeDisposable);
            ConfirmActionCommand = NoggogCommand.CreateFromObject(
                objectSource: this.WhenAnyFallback(x => x.TargetConfirmation !.ToDo),
                canExecute: toDo => toDo != null,
                execute: toDo =>
            {
                toDo?.Invoke();
                TargetConfirmation = null;
            },
                disposable: this.CompositeDisposable);

            _Hot = this.WhenAnyValue(x => x.ActivePanel)
                   .Select(x =>
            {
                switch (x)
                {
                case ConfigurationVM config:
                    return(config.WhenAnyFallback(x => x.CurrentRun !.Running, fallback: false));

                case PatchersRunVM running:
                    return(running.WhenAnyValue(x => x.Running));

                default:
                    break;
                }
                return(Observable.Return(false));
            })
                   .Switch()
                   .DistinctUntilChanged()
                   .ToGuiProperty(this, nameof(Hot));

            OpenProfilesPageCommand = ReactiveCommand.Create(() =>
            {
                ActivePanel = new ProfilesDisplayVM(Configuration, ActivePanel);
            },
                                                             canExecute: Observable.CombineLatest(
                                                                 this.WhenAnyFallback(x => x.Configuration.CurrentRun !.Running, fallback: false),
                                                                 this.WhenAnyValue(x => x.ActivePanel)
                                                                 .Select(x => x is ProfilesDisplayVM),
                                                                 (running, isProfile) => !running && !isProfile));

            IdeOptions.AddRange(EnumExt.GetValues <IDE>());

            Task.Run(() => Mutagen.Bethesda.WarmupAll.Init()).FireAndForget();

            SynthesisVersion = Mutagen.Bethesda.Synthesis.Versions.SynthesisVersion;
            MutagenVersion   = Mutagen.Bethesda.Synthesis.Versions.MutagenVersion;

            var latestVersions = Observable.Return(Unit.Default)
                                 .ObserveOn(RxApp.TaskpoolScheduler)
                                 .CombineLatest(
                DotNetSdkInstalled,
                (_, v) => v)
                                 .SelectTask(async v =>
            {
                var normalUpdateTask     = GetLatestVersions(v, includePrerelease: false);
                var prereleaseUpdateTask = GetLatestVersions(v, includePrerelease: true);
                await Task.WhenAll(normalUpdateTask, prereleaseUpdateTask);
                return(Normal: await normalUpdateTask, Prerelease: await prereleaseUpdateTask);
            })
                                 .Replay(1)
                                 .RefCount();

            NewestMutagenVersion = Observable.CombineLatest(
                latestVersions,
                this.WhenAnyFallback(x => x.Configuration.SelectedProfile !.ConsiderPrereleaseNugets),
                (vers, prereleases) => prereleases ? vers.Prerelease.MutagenVersion : vers.Normal.MutagenVersion)
                                   .Replay(1)
                                   .RefCount();
            NewestSynthesisVersion = Observable.CombineLatest(
                latestVersions,
                this.WhenAnyFallback(x => x.Configuration.SelectedProfile !.ConsiderPrereleaseNugets),
                (vers, prereleases) => prereleases ? vers.Prerelease.SynthesisVersion : vers.Normal.SynthesisVersion)
                                     .Replay(1)
                                     .RefCount();

            // Switch to DotNet screen if missing
            DotNetSdkInstalled
            .Subscribe(v =>
            {
                if (v == null)
                {
                    ActivePanel = new DotNetNotInstalledVM(this, this.ActivePanel, DotNetSdkInstalled);
                }
            });

            _ActiveConfirmation = Observable.CombineLatest(
                this.WhenAnyFallback(x => x.Configuration.SelectedProfile !.SelectedPatcher)
                .Select(x =>
            {
                if (x is not GitPatcherVM gitPatcher)
                {
                    return(Observable.Return(default(GitPatcherVM?)));
                }
                return(gitPatcher.WhenAnyValue(x => x.PatcherSettings.SettingsOpen)
                       .Select(open => open ? (GitPatcherVM?)gitPatcher : null));
            })
                .Switch(),
                this.WhenAnyValue(x => x.TargetConfirmation),
                (openPatcher, target) =>
            {
                if (target != null)
                {
                    return(target);
                }
                if (openPatcher == null)
                {
                    return(default(ConfirmationActionVM?));
                }
                return(new ConfirmationActionVM(
                           "External Patcher Settings Open",
                           $"{openPatcher.Nickname} is open for settings manipulation.",
                           toDo: null));
            })
                                  .ToGuiProperty(this, nameof(ActiveConfirmation), default(ConfirmationActionVM?));

            _InModal = this.WhenAnyValue(x => x.ActiveConfirmation)
                       .Select(x => x != null)
                       .ToGuiProperty(this, nameof(InModal));
        }
Ejemplo n.º 6
0
        public ProfileVM(ConfigurationVM parent, GameRelease?release = null, string?id = null)
        {
            ID      = id ?? Guid.NewGuid().ToString();
            Config  = parent;
            Release = release ?? GameRelease.Oblivion;
            AddGitPatcherCommand      = ReactiveCommand.Create(() => SetInitializer(new GitPatcherInitVM(this)));
            AddSolutionPatcherCommand = ReactiveCommand.Create(() => SetInitializer(new SolutionPatcherInitVM(this)));
            AddCliPatcherCommand      = ReactiveCommand.Create(() => SetInitializer(new CliPatcherInitVM(this)));
            AddSnippetPatcherCommand  = ReactiveCommand.Create(() => SetPatcherForInitialConfiguration(new CodeSnippetPatcherVM(this)));

            ProfileDirectory = Path.Combine(Execution.Constants.WorkingDirectory, ID);
            WorkingDirectory = Execution.Constants.ProfileWorkingDirectory(ID);

            var dataFolderResult = this.WhenAnyValue(x => x.Release)
                                   .ObserveOn(RxApp.TaskpoolScheduler)
                                   .Select(x =>
            {
                try
                {
                    return(GetResponse <string> .Succeed(
                               Path.Combine(x.ToWjGame().MetaData().GameLocation().ToString(), "Data")));
                }
                catch (Exception ex)
                {
                    return(GetResponse <string> .Fail(string.Empty, ex));
                }
            })
                                   .Replay(1)
                                   .RefCount();

            _DataFolder = dataFolderResult
                          .Select(x => x.Value)
                          .ToGuiProperty <string>(this, nameof(DataFolder));

            var loadOrderResult = Observable.CombineLatest(
                this.WhenAnyValue(x => x.Release),
                dataFolderResult,
                (release, dataFolder) => (release, dataFolder))
                                  .ObserveOn(RxApp.TaskpoolScheduler)
                                  .Select(x =>
            {
                if (x.dataFolder.Failed)
                {
                    return(Results: Observable.Empty <IChangeSet <LoadOrderListing> >(), State: Observable.Return <ErrorResponse>(ErrorResponse.Fail("Data folder not set")));
                }
                var path = Mutagen.Bethesda.LoadOrder.GetPluginsPath(x.release);
                return(Results: Mutagen.Bethesda.LoadOrder.GetLiveLoadOrder(x.release, path, x.dataFolder.Value, out var errors), State: errors);
            })
                                  .Replay(1)
                                  .RefCount();

            LoadOrder = loadOrderResult
                        .Select(x => x.Results)
                        .Switch()
                        .AsObservableList();

            _LargeOverallError = Observable.CombineLatest(
                dataFolderResult,
                loadOrderResult
                .Select(x => x.State)
                .Switch(),
                Patchers.Connect()
                .AutoRefresh(x => x.IsOn)
                .Filter(p => p.IsOn)
                .AutoRefresh(x => x.State)
                .QueryWhenChanged(q => q)
                .StartWith(Noggog.ListExt.Empty <PatcherVM>()),
                (dataFolder, loadOrder, coll) =>
            {
                if (coll.Count == 0)
                {
                    return(GetResponse <PatcherVM> .Fail("There are no enabled patchers to run."));
                }
                if (!dataFolder.Succeeded)
                {
                    return(dataFolder.BubbleFailure <PatcherVM>());
                }
                if (!loadOrder.Succeeded)
                {
                    return(loadOrder.BubbleFailure <PatcherVM>());
                }
                var blockingError = coll.FirstOrDefault(p => p.State.IsHaltingError);
                if (blockingError != null)
                {
                    return(GetResponse <PatcherVM> .Fail(blockingError, $"\"{blockingError.DisplayName}\" has a blocking error"));
                }
                return(GetResponse <PatcherVM> .Succeed(null !));
            })
Ejemplo n.º 7
0
        public PatchersRunVM(ConfigurationVM parent, ProfileVM profile)
        {
            Config         = parent;
            RunningProfile = profile;
            Patchers.AddOrUpdate(RunningProfile.Patchers.Items
                                 .Where(x => x.IsOn)
                                 .Select(p => p.ToRunner(this)));
            PatchersDisplay = Patchers.Connect()
                              .ToObservableCollection(this);
            if (parent.SelectedPatcher != null &&
                Patchers.TryGetValue(parent.SelectedPatcher.InternalID, out var run))
            {
                SelectedPatcher = run;
            }

            BackCommand = ReactiveCommand.Create(() =>
            {
                parent.SelectedPatcher    = SelectedPatcher?.Config;
                parent.MainVM.ActivePanel = parent;
            },
                                                 canExecute: this.WhenAnyValue(x => x.Running)
                                                 .Select(running => !running));
            CancelCommand = ReactiveCommand.CreateFromTask(
                execute: Cancel,
                canExecute: this.WhenAnyValue(x => x.Running));

            _reporter.Overall
            .ObserveOnGui()
            .Subscribe(ex =>
            {
                Log.Logger.Error(ex, "Error while running patcher pipeline");
                ResultError = ex;
            })
            .DisposeWith(this);
            _reporter.PrepProblem
            .Select(data => (data, type: "prepping"))
            .Merge(_reporter.RunProblem
                   .Select(data => (data, type: "running")))
            .ObserveOnGui()
            .Subscribe(i =>
            {
                var vm          = Patchers.Get(i.data.Key);
                vm.State        = GetResponse <RunState> .Fail(RunState.Error, i.data.Error);
                SelectedPatcher = vm;
                Log.Logger
                .ForContext(nameof(PatcherVM.DisplayName), i.data.Run.Name)
                .Error(i.data.Error, $"Error while prepping {i.type}");
            })
            .DisposeWith(this);
            _reporter.Starting
            .ObserveOnGui()
            .Subscribe(i =>
            {
                var vm   = Patchers.Get(i.Key);
                vm.State = GetResponse <RunState> .Succeed(RunState.Started);
                Log.Logger
                .ForContext(nameof(PatcherVM.DisplayName), i.Run.Name)
                .Information($"Starting");
            })
            .DisposeWith(this);
            _reporter.RunSuccessful
            .ObserveOnGui()
            .Subscribe(i =>
            {
                var vm   = Patchers.Get(i.Key);
                vm.State = GetResponse <RunState> .Succeed(RunState.Finished);
                Log.Logger
                .ForContext(nameof(PatcherVM.DisplayName), i.Run.Name)
                .Information("Finished {RunTime}", vm.RunTime);
            })
            .DisposeWith(this);
            _reporter.Output
            .Subscribe(s =>
            {
                Log.Logger
                .ForContextIfNotNull(nameof(PatcherVM.DisplayName), s.Run?.Name)
                .Information(s.String);
            })
            .DisposeWith(this);
            _reporter.Error
            .Subscribe(s =>
            {
                Log.Logger
                .ForContextIfNotNull(nameof(PatcherVM.DisplayName), s.Run?.Name)
                .Error(s.String);
            })
            .DisposeWith(this);

            // Clear selected patcher on showing error
            this.ShowOverallErrorCommand.StartingExecution()
            .Subscribe(_ => this.SelectedPatcher = null)
            .DisposeWith(this);

            _DetailDisplay = Observable.Merge(
                this.WhenAnyValue(x => x.SelectedPatcher)
                .Select(i => i as object),
                this.ShowOverallErrorCommand.EndingExecution()
                .Select(_ => ResultError == null ? null : new OverallErrorVM(ResultError)))
                             .ToGuiProperty(this, nameof(DetailDisplay));
        }
Ejemplo n.º 8
0
        public MainVM()
        {
            var dotNet = Observable.Interval(TimeSpan.FromSeconds(10), RxApp.TaskpoolScheduler)
                         .StartWith(0)
                         .SelectTask(async i =>
            {
                try
                {
                    var ret = await DotNetQueries.DotNetSdkVersion();
                    Log.Logger.Information($"dotnet SDK: {ret}");
                    return(ret);
                }
                catch (Exception ex)
                {
                    Log.Logger.Error(ex, $"Error retrieving dotnet SDK version");
                    return(default(Version?));
                }
            });

            DotNetSdkInstalled = dotNet
                                 .Take(1)
                                 .Merge(dotNet
                                        .FirstAsync(v => v != null))
                                 .DistinctUntilChanged()
                                 .Replay(1)
                                 .RefCount();

            Configuration        = new ConfigurationVM(this);
            ActivePanel          = Configuration;
            DiscardActionCommand = ReactiveCommand.Create(() => ActiveConfirmation = null);
            ConfirmActionCommand = ReactiveCommand.Create(
                () =>
            {
                if (ActiveConfirmation == null)
                {
                    return;
                }
                ActiveConfirmation.ToDo();
                ActiveConfirmation = null;
            });

            _Hot = this.WhenAnyValue(x => x.ActivePanel)
                   .Select(x =>
            {
                switch (x)
                {
                case ConfigurationVM config:
                    return(config.WhenAnyFallback(x => x.CurrentRun !.Running, fallback: false));

                case PatchersRunVM running:
                    return(running.WhenAnyValue(x => x.Running));

                default:
                    break;
                }
                return(Observable.Return(false));
            })
                   .Switch()
                   .DistinctUntilChanged()
                   .ToGuiProperty(this, nameof(Hot));

            OpenProfilesPageCommand = ReactiveCommand.Create(() =>
            {
                ActivePanel = new ProfilesDisplayVM(Configuration, ActivePanel);
            },
                                                             canExecute: Observable.CombineLatest(
                                                                 this.WhenAnyFallback(x => x.Configuration.CurrentRun !.Running, fallback: false),
                                                                 this.WhenAnyValue(x => x.ActivePanel)
                                                                 .Select(x => x is ProfilesDisplayVM),
                                                                 (running, isProfile) => !running && !isProfile));

            IdeOptions.AddRange(EnumExt.GetValues <IDE>());

            Task.Run(() => Mutagen.Bethesda.WarmupAll.Init()).FireAndForget();

            SynthesisVersion = Mutagen.Bethesda.Synthesis.Versions.SynthesisVersion;
            MutagenVersion   = Mutagen.Bethesda.Synthesis.Versions.MutagenVersion;

            var latestVersions = Observable.Return(Unit.Default)
                                 .ObserveOn(RxApp.TaskpoolScheduler)
                                 .CombineLatest(
                DotNetSdkInstalled,
                (_, v) => v)
                                 .SelectTask(GetLatestVersions)
                                 .Replay(1)
                                 .RefCount();

            NewestMutagenVersion = latestVersions
                                   .Select(x => x.MutagenVersion);
            NewestSynthesisVersion = latestVersions
                                     .Select(x => x.SynthesisVersion);

            // Switch to DotNet screen if missing
            DotNetSdkInstalled
            .Subscribe(v =>
            {
                if (v == null)
                {
                    ActivePanel = new DotNetNotInstalledVM(this, this.ActivePanel, DotNetSdkInstalled);
                }
            });
        }