From 394b080b54c82e0996f886de2437e08b1d1dcb5e Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Wed, 1 May 2024 02:27:18 +0100 Subject: [PATCH 01/19] Added: Support for 'Marker Loadouts' in Loadout Deletion & Automatic Switch from Removed Loadout --- .../ALoadoutSynchronizer.cs | 139 ++++++++++++------ .../Loadout.cs | 9 ++ .../Controls/Spine/SpineViewModel.cs | 3 +- .../Loadout/LoadoutLeftMenuViewModel.cs | 27 +++- .../Pages/MyGames/MyGamesViewModel.cs | 38 ++--- .../Verbs/LoadoutManagementVerbs.cs | 6 +- 6 files changed, 154 insertions(+), 68 deletions(-) diff --git a/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs b/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs index b9dbc4ffff..89d620da3d 100644 --- a/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs +++ b/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Diagnostics; +using System.Net.Security; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Microsoft.Extensions.DependencyInjection; @@ -591,6 +592,11 @@ public virtual async Task Apply(Loadout loadout, bool forceSkipIn var diskState = await FileTreeToDisk(fileTree, loadout, flattened, prevState, loadout.Installation, forceSkipIngest); diskState.LoadoutRevision = loadout.DataStoreId; await _diskStateRegistry.SaveState(loadout.Installation, diskState); + + // Remove an existing temporary marker loadout. + if (!loadout.IsMarkerLoadout) + RemoveMarkerLoadout(); + return diskState; } @@ -840,12 +846,14 @@ private Mod CreateGameFilesMod(DiskStateTree initialState) /// public async Task UnManage(GameInstallation installation) { - await ResetToInitialState(installation); + await FastForceApplyOriginalState(installation); + + // Cleanup all of the metadata left behind for this game. + // All database information, including loadouts, initial game state and + // TODO: Garbage Collect unused files. foreach (var loadoutId in _loadoutRegistry.AllLoadoutIds().ToArray()) _loadoutRegistry.Delete(loadoutId); - // Now clear the vanilla game state. - // We don't want the user's folder to reset. await _diskStateRegistry.ClearInitialState(installation); } @@ -857,65 +865,74 @@ public async Task DeleteLoadout(GameInstallation installation, LoadoutId id) var installLocation = installation.LocationsRegister[LocationId.Game].ToString(); var wasLastLoadout = _loadoutRegistry .AllLoadouts() - .Count(x => x.Installation.LocationsRegister[LocationId.Game].ToString() == installLocation) <= 1; + .Count(x => + x.Installation.LocationsRegister[LocationId.Game].ToString() == installLocation + && !x.IsMarkerLoadout) <= 1; if (_loadoutRegistry.IsApplied(id, _diskStateRegistry.GetLastAppliedLoadout(installation))) { /* Note(Sewer) - The loadout being deleted is the currently active loadout + The loadout being deleted is the currently active loadout. + As a 'default' reasonable behaviour, we will reset the game folder - to its initial state. This is a good default as in most cases, - game files are not likely to be overwritten, so this will just - end up materialising into a bunch of deletes. (Very Fast) + to its initial state and create a 'hidden' loadout to accomodate this. - In the future, we may make a setting to change the behaviour, - if for example the user wants it to revert to last applied loadout - that isn't the one being deleted. + This is a good default for many cases: + + - Game files are not likely to be overwritten, so this will + just end up materialising into a bunch of deletes. (Very Fast) + + - Ensures internal consistency. i.e. 'last applied loadout' is always + a valid loadout. + + - Provides more backend flexibility (e.g. we can 'squash' the + revisions at the beginning of other loadouts without consequence.) + + - Meets user UI/UX expectations. The next loadout they navigate to + won't be somehow magically applied. + + We may make a setting to change the behaviour in the future, + via a setting to match user preferences, but for now this is the + default. */ - await ResetToInitialState(installation); + var markerLoadout = await CreateMarkerLoadout(installation); + await Apply(markerLoadout, true); } _loadoutRegistry.Delete(id); if (wasLastLoadout) await _diskStateRegistry.ClearInitialState(installation); } - - private async Task ResetToInitialState(GameInstallation installation) + + /// + /// Creates a 'Marker Loadout', which is a loadout embued with the initial + /// state of the game folder. + /// + /// This loadout is created when the last applied loadout for a game + /// is deleted. And is deleted when a non-marker loadout is applied. + /// It should be a singleton. + /// + private async Task CreateMarkerLoadout(GameInstallation installation) { - var (isCached, initialState) = await GetOrCreateInitialDiskState(installation); - if (!isCached) - throw new InvalidOperationException("Something is very wrong with the DataStore.\n" + - "We don't have an initial disk state for the game folder, but we should have.\n" + - "Because this disk state should only ever be deleted when unmanaging a game."); - - /* - Note (Sewer) - - Normally we could navigate to the very first revision of a loadout - to get the 'vanilla' state of the game folder and apply that, however... + var initialState = await GetOrCreateInitialDiskState(installation); + var loadoutId = LoadoutId.Create(); + var gameFiles = CreateGameFilesMod(initialState.tree); + var fileTree = CreateFileTreeFromMod(gameFiles); + await BackupNewFiles(installation, fileTree); - In the future we will support rollbacks for loadouts. - - That means the user will be able to update the 'starting' - state of a given loadout, deleting previous history to save space. - - In order to keep future code 'simpler', where we won't have to make - special exceptions/logic. We just perform a stripped down apply - with the original file tree here. - */ + var loadout = _loadoutRegistry.Alter(loadoutId, "Marker Loadout", loadout => loadout with + { + Name = "Marker Loadout", + Installation = installation, + Mods = loadout.Mods.With(gameFiles.Id, gameFiles), + IsMarkerLoadout = true, + }); - // Create a non-persisted mod from original game files. - // Then effectively 'Apply' it as single mod 'Loadout'. - var gameFilesMod = CreateGameFilesMod(initialState); - var fileTree = CreateFileTreeFromMod(gameFilesMod); - var flattened = ModsToFlattenedLoadout([gameFilesMod]); - var prevState = _diskStateRegistry.GetState(installation)!; - await FileTreeToDisk(fileTree, null, flattened, prevState, installation, true); + return loadout; } - #endregion #region FlattenLoadoutToTree Methods @@ -936,7 +953,6 @@ protected virtual async ValueTask[]> ModSortRules(Loadout return await builtInSortRules.ToAsyncEnumerable().Concat(customSortRules).ToArrayAsync(); } - /// /// Sorts the mods in a loadout. /// @@ -959,6 +975,21 @@ protected virtual async Task> SortMods(Loadout loadout) #region Misc Helper Functions + /// + /// Removes the first found 'marker' loadout from the data store. + /// + /// + /// The 'marker loadout' should be a singleton as per + /// and . + /// + private void RemoveMarkerLoadout() + { + foreach (var loadout in _loadoutRegistry.AllLoadouts()) + { + if (loadout.IsMarkerLoadout) + _loadoutRegistry.Delete(loadout.LoadoutId); + } + } /// /// By default this method just returns the current state of the game folders. Most of the time @@ -981,6 +1012,28 @@ protected virtual async Task> SortMods(Loadout loadout) await _diskStateRegistry.SaveInitialState(installation, indexedState); return (false, indexedState); } + + /// + /// Quickly reverts the folder to its original state, without updating + /// the database state or doing any other changes that produce + /// external side effects. + /// + /// Intended for use when unmanaging a game. + /// + private async Task FastForceApplyOriginalState(GameInstallation installation) + { + var (isCached, initialState) = await GetOrCreateInitialDiskState(installation); + if (!isCached) + throw new InvalidOperationException("Something is very wrong with the DataStore.\n" + + "We don't have an initial disk state for the game folder, but we should have.\n" + + "Because this disk state should only ever be deleted when unmanaging a game."); + + var gameFilesMod = CreateGameFilesMod(initialState); + var fileTree = CreateFileTreeFromMod(gameFilesMod); + var flattened = ModsToFlattenedLoadout([gameFilesMod]); + var prevState = _diskStateRegistry.GetState(installation)!; + await FileTreeToDisk(fileTree, null, flattened, prevState, installation, true); + } #endregion #region Internal Helper Functions diff --git a/src/Abstractions/NexusMods.Abstractions.Loadouts/Loadout.cs b/src/Abstractions/NexusMods.Abstractions.Loadouts/Loadout.cs index ecfe08e569..d88d44b527 100644 --- a/src/Abstractions/NexusMods.Abstractions.Loadouts/Loadout.cs +++ b/src/Abstractions/NexusMods.Abstractions.Loadouts/Loadout.cs @@ -46,6 +46,15 @@ public record Loadout : Entity, IEmptyWithDataStore /// Link to the previous version of this loadout on the data store. /// public required EntityLink PreviousVersion { get; init; } + + /// + /// This is true if the loadout is a hidden 'Marker' loadout. + /// A marker loadout is created from the original game state and should + /// be a singleton. + /// + /// Marker loadouts should not be shown in any user facing elements. + /// + public bool IsMarkerLoadout { get; init; } /// public override EntityCategory Category => EntityCategory.Loadouts; diff --git a/src/NexusMods.App.UI/Controls/Spine/SpineViewModel.cs b/src/NexusMods.App.UI/Controls/Spine/SpineViewModel.cs index ec28d4e741..a89810c244 100644 --- a/src/NexusMods.App.UI/Controls/Spine/SpineViewModel.cs +++ b/src/NexusMods.App.UI/Controls/Spine/SpineViewModel.cs @@ -69,7 +69,6 @@ public SpineViewModel( Downloads.WorkspaceContext = new DownloadsContext(); _specialSpineItems.Add(Downloads); Downloads.Click = ReactiveCommand.Create(NavigateToDownloads); - if (!_windowManager.TryGetActiveWindow(out var currentWindow)) return; var workspaceController = currentWindow.WorkspaceController; @@ -78,7 +77,7 @@ public SpineViewModel( { loadoutRegistry.LoadoutRootChanges .Transform(loadoutId => (loadoutId, loadout: loadoutRegistry.Get(loadoutId))) - .Filter(tuple => tuple.loadout != null) + .Filter(tuple => tuple.loadout is { IsMarkerLoadout: false }) .TransformAsync(async tuple => { var loadoutId = tuple.loadoutId; diff --git a/src/NexusMods.App.UI/LeftMenu/Loadout/LoadoutLeftMenuViewModel.cs b/src/NexusMods.App.UI/LeftMenu/Loadout/LoadoutLeftMenuViewModel.cs index 3e30e2ab08..ad7047fb06 100644 --- a/src/NexusMods.App.UI/LeftMenu/Loadout/LoadoutLeftMenuViewModel.cs +++ b/src/NexusMods.App.UI/LeftMenu/Loadout/LoadoutLeftMenuViewModel.cs @@ -1,13 +1,17 @@ using System.Collections.ObjectModel; using System.Reactive.Disposables; +using System.Reactive.Linq; +using DynamicData; using DynamicData.Kernel; using Microsoft.Extensions.DependencyInjection; using NexusMods.Abstractions.Diagnostics; +using NexusMods.Abstractions.Loadouts; using NexusMods.App.UI.Controls.Navigation; using NexusMods.App.UI.LeftMenu.Items; using NexusMods.App.UI.Pages.Diagnostics; using NexusMods.App.UI.Pages.LoadoutGrid; using NexusMods.App.UI.Pages.ModLibrary; +using NexusMods.App.UI.Pages.MyGames; using NexusMods.App.UI.Resources; using NexusMods.App.UI.WorkspaceSystem; using NexusMods.Icons; @@ -29,7 +33,8 @@ public LoadoutLeftMenuViewModel( IServiceProvider serviceProvider) { var diagnosticManager = serviceProvider.GetRequiredService(); - + var loadoutRegistry = serviceProvider.GetRequiredService(); + WorkspaceId = workspaceId; ApplyControlViewModel = new ApplyControlViewModel(loadoutContext.LoadoutId, serviceProvider); @@ -113,6 +118,26 @@ public LoadoutLeftMenuViewModel( diagnosticItem.Name = $"{Language.LoadoutLeftMenuViewModel_LoadoutLeftMenuViewModel_Diagnostics} ({totalCount})"; }) .DisposeWith(disposable); + + loadoutRegistry.LoadoutRootChanges + .Where(changeSet => changeSet.Any(change => change.Reason == ListChangeReason.Remove)) + .OnUI() + .Subscribe(_ => + { + // Loadout is deleted, check if the current workspace is + // associated with the deleted loadout And if it is, navigate + // away. For now, we will navigate to Home ('My Games'), + if (workspaceController.ActiveWorkspace?.Context is LoadoutContext activeLoadoutContext && + activeLoadoutContext.LoadoutId == loadoutContext.LoadoutId) + { + workspaceController.ChangeOrCreateWorkspaceByContext(() => new PageData + { + FactoryId = MyGamesPageFactory.StaticId, + Context = new MyGamesPageContext(), + }); + } + }) + .DisposeWith(disposable); }); } } diff --git a/src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs b/src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs index 6577b52013..1b187de25d 100644 --- a/src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs +++ b/src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs @@ -137,27 +137,27 @@ private async Task ManageGame(GameInstallation installation) var marker = await _loadoutRegistry.Manage(installation, name); var loadoutId = marker.Id; + Dispatcher.UIThread.Invoke(() => { SwitchToLoadoutId(loadoutId); }); + } - Dispatcher.UIThread.Invoke(() => - { - if (!_windowManager.TryGetActiveWindow(out var window)) return; - var workspaceController = window.WorkspaceController; + private void SwitchToLoadoutId(LoadoutId loadoutId) + { + if (!_windowManager.TryGetActiveWindow(out var window)) return; + var workspaceController = window.WorkspaceController; - workspaceController.ChangeOrCreateWorkspaceByContext( - context => context.LoadoutId == loadoutId, - () => new PageData - { - FactoryId = LoadoutGridPageFactory.StaticId, - Context = new LoadoutGridContext - { - LoadoutId = loadoutId - } - }, - () => new LoadoutContext - { - LoadoutId = loadoutId - } - ); + workspaceController.ChangeOrCreateWorkspaceByContext( + context => context.LoadoutId == loadoutId, + () => new PageData + { + FactoryId = LoadoutGridPageFactory.StaticId, + Context = new LoadoutGridContext + { + LoadoutId = loadoutId + } + }, + () => new LoadoutContext + { + LoadoutId = loadoutId } ); } diff --git a/src/NexusMods.DataModel/CommandLine/Verbs/LoadoutManagementVerbs.cs b/src/NexusMods.DataModel/CommandLine/Verbs/LoadoutManagementVerbs.cs index d3b274d017..63f383fca7 100644 --- a/src/NexusMods.DataModel/CommandLine/Verbs/LoadoutManagementVerbs.cs +++ b/src/NexusMods.DataModel/CommandLine/Verbs/LoadoutManagementVerbs.cs @@ -148,10 +148,11 @@ private static async Task ListLoadouts([Injected] IRenderer renderer, [Injected] CancellationToken token) { var rows = registry.AllLoadouts() + .Where(x => !x.IsMarkerLoadout) .Select(list => new object[] { list.Name, list.Installation, list.LoadoutId, list.Mods.Count }) .ToList(); - await renderer.Table(new[] { "Name", "Game", "Id", "Mod Count" }, rows); + await renderer.Table(["Name", "Game", "Id", "Mod Count"], rows); return 0; } @@ -227,7 +228,7 @@ private static async Task ManageGame([Injected] IRenderer renderer, [Verb("remove-loadout", "Remove a loadout by its ID")] private static async Task RemoveLoadout( [Injected] IRenderer renderer, - [Option("l", "loadout", "loadout to add the mod to")] LoadoutMarker loadoutMarker, + [Option("l", "loadout", "loadout to remove.")] LoadoutMarker loadoutMarker, [Injected] LoadoutRegistry registry, [Injected] CancellationToken token) { @@ -241,7 +242,6 @@ private static async Task RemoveLoadout( var installation = loadout.Installation; var synchronizer = installation.GetGame().Synchronizer; await synchronizer.DeleteLoadout(installation, loadoutId); - registry.Delete(loadoutId); await renderer.Text($"Loadout {loadoutId} removed successfully"); return 0; } From 7498dba18f46f1f1a855536f989f80fc4ce48c48 Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Wed, 1 May 2024 03:50:43 +0100 Subject: [PATCH 02/19] Added: Sanitization of Invalid Workspace Data --- .../Windows/DesignWindowManager.cs | 4 +- .../Windows/IWindowManager.cs | 4 +- .../Windows/MainWindowViewModel.cs | 54 ++++++++++++++++++- src/NexusMods.App.UI/Windows/WindowManager.cs | 3 +- 4 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/NexusMods.App.UI/Windows/DesignWindowManager.cs b/src/NexusMods.App.UI/Windows/DesignWindowManager.cs index 110dfe3dd2..dbe9d2a726 100644 --- a/src/NexusMods.App.UI/Windows/DesignWindowManager.cs +++ b/src/NexusMods.App.UI/Windows/DesignWindowManager.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; +using NexusMods.App.UI.WorkspaceSystem; namespace NexusMods.App.UI.Windows; @@ -28,6 +29,5 @@ public void RegisterWindow(IWorkspaceWindow window) { } public void UnregisterWindow(IWorkspaceWindow window) { } public void SaveWindowState(IWorkspaceWindow window) { } - - public bool RestoreWindowState(IWorkspaceWindow window) => false; + public bool RestoreWindowState(IWorkspaceWindow window, Func? sanitize = null) => false; } diff --git a/src/NexusMods.App.UI/Windows/IWindowManager.cs b/src/NexusMods.App.UI/Windows/IWindowManager.cs index 8f1614edd7..331ece8e9a 100644 --- a/src/NexusMods.App.UI/Windows/IWindowManager.cs +++ b/src/NexusMods.App.UI/Windows/IWindowManager.cs @@ -1,6 +1,7 @@ using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using JetBrains.Annotations; +using NexusMods.App.UI.WorkspaceSystem; namespace NexusMods.App.UI.Windows; @@ -53,6 +54,7 @@ public interface IWindowManager /// Restores the saved window state. /// /// + /// Optional method to sanitize the restored data. /// Whether the restore was successful. - public bool RestoreWindowState(IWorkspaceWindow window); + public bool RestoreWindowState(IWorkspaceWindow window, Func? sanitize = null); } diff --git a/src/NexusMods.App.UI/Windows/MainWindowViewModel.cs b/src/NexusMods.App.UI/Windows/MainWindowViewModel.cs index bd763a0f59..5b5c14b369 100644 --- a/src/NexusMods.App.UI/Windows/MainWindowViewModel.cs +++ b/src/NexusMods.App.UI/Windows/MainWindowViewModel.cs @@ -112,13 +112,65 @@ public MainWindowViewModel( vm._windowManager.UnregisterWindow(vm); }).DisposeWith(d); - if (!_windowManager.RestoreWindowState(this)) + if (!_windowManager.RestoreWindowState(this, SanitizeWindowData)) { // NOTE(erri120): select home on startup if we didn't restore the previous state Spine.NavigateToHome(); } }); } + + private WindowData SanitizeWindowData(WindowData data) + { + /* + Note(Sewer) + + Some of our persisted workspaces are dependent on the contents of the + datastore. For example, we create a workspace for each loadout. + + In some rare scenarios, it may however become possible that our + saved window state is not valid. + + For loadouts, some examples would be: + + - User deleted a loadout from the CLI + - Which may not remove the workspace. + - App crashes before complete removal of a loadout is complete. + + Although we could reorder operations, or add explicit code to update + the current workspaces from say, the CLI command; there's still + some inherent risks. + + Suppose someone writes some code that could run before the UI is + fully set up. They would have to remember to update workspaces. + If they forget, there's a chance of creating a potentially + very silent, hard to find bug. + + Therefore, we sanitize the data here, doing cleanup on no longer + valid items. + */ + + var workspaces = new List(data.Workspaces.Length); + var activeWorkspaceId = data.ActiveWorkspaceId; + foreach (var workspace in data.Workspaces) + { + if (workspace.Context is LoadoutContext loadout && !_registry.Contains(loadout.LoadoutId)) + { + if (activeWorkspaceId == workspace.Id) + activeWorkspaceId = null; + + continue; + } + + workspaces.Add(workspace); + } + + return data with + { + Workspaces = workspaces.ToArray(), + ActiveWorkspaceId = activeWorkspaceId, + }; + } internal void OnClose() { diff --git a/src/NexusMods.App.UI/Windows/WindowManager.cs b/src/NexusMods.App.UI/Windows/WindowManager.cs index f65dfaf527..72f1a67ab8 100644 --- a/src/NexusMods.App.UI/Windows/WindowManager.cs +++ b/src/NexusMods.App.UI/Windows/WindowManager.cs @@ -94,13 +94,14 @@ public void SaveWindowState(IWorkspaceWindow window) } } - public bool RestoreWindowState(IWorkspaceWindow window) + public bool RestoreWindowState(IWorkspaceWindow window, Func? sanitize) { try { var data = _dataStore.Get(WindowData.Id); if (data is null) return false; + data = sanitize != null ? sanitize(data) : data; window.WorkspaceController.FromData(data); return true; } From 1093d0dc67b2032b5c1791ad14ab27911dac5bf6 Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Wed, 1 May 2024 04:34:59 +0100 Subject: [PATCH 03/19] Refactored: Workspace Switcher on Loadout Delete --- .../LeftMenu/Loadout/LoadoutLeftMenuViewModel.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/NexusMods.App.UI/LeftMenu/Loadout/LoadoutLeftMenuViewModel.cs b/src/NexusMods.App.UI/LeftMenu/Loadout/LoadoutLeftMenuViewModel.cs index ad7047fb06..b4dfdc7178 100644 --- a/src/NexusMods.App.UI/LeftMenu/Loadout/LoadoutLeftMenuViewModel.cs +++ b/src/NexusMods.App.UI/LeftMenu/Loadout/LoadoutLeftMenuViewModel.cs @@ -120,15 +120,10 @@ public LoadoutLeftMenuViewModel( .DisposeWith(disposable); loadoutRegistry.LoadoutRootChanges - .Where(changeSet => changeSet.Any(change => change.Reason == ListChangeReason.Remove)) - .OnUI() - .Subscribe(_ => + .OnItemRemoved(loadoutId => { - // Loadout is deleted, check if the current workspace is - // associated with the deleted loadout And if it is, navigate - // away. For now, we will navigate to Home ('My Games'), if (workspaceController.ActiveWorkspace?.Context is LoadoutContext activeLoadoutContext && - activeLoadoutContext.LoadoutId == loadoutContext.LoadoutId) + activeLoadoutContext.LoadoutId == loadoutId) { workspaceController.ChangeOrCreateWorkspaceByContext(() => new PageData { @@ -136,7 +131,9 @@ public LoadoutLeftMenuViewModel( Context = new MyGamesPageContext(), }); } - }) + }, false) + .OnUI() + .SubscribeWithErrorLogging() .DisposeWith(disposable); }); } From 9cde0bc4e4734418d20632b8ee9f6ed60d03d4d1 Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Wed, 1 May 2024 04:56:08 +0100 Subject: [PATCH 04/19] Fixed: Event Order of Loadout Remove Callback --- .../LeftMenu/Loadout/LoadoutLeftMenuViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NexusMods.App.UI/LeftMenu/Loadout/LoadoutLeftMenuViewModel.cs b/src/NexusMods.App.UI/LeftMenu/Loadout/LoadoutLeftMenuViewModel.cs index b4dfdc7178..1c91a5c37c 100644 --- a/src/NexusMods.App.UI/LeftMenu/Loadout/LoadoutLeftMenuViewModel.cs +++ b/src/NexusMods.App.UI/LeftMenu/Loadout/LoadoutLeftMenuViewModel.cs @@ -120,6 +120,7 @@ public LoadoutLeftMenuViewModel( .DisposeWith(disposable); loadoutRegistry.LoadoutRootChanges + .OnUI() .OnItemRemoved(loadoutId => { if (workspaceController.ActiveWorkspace?.Context is LoadoutContext activeLoadoutContext && @@ -132,7 +133,6 @@ public LoadoutLeftMenuViewModel( }); } }, false) - .OnUI() .SubscribeWithErrorLogging() .DisposeWith(disposable); }); From 0afe03273f52785938af3cfaaa50f849513b9c97 Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Wed, 1 May 2024 05:01:40 +0100 Subject: [PATCH 05/19] Fixed: We should be prematurely returning here. --- .../ALoadoutSynchronizer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs b/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs index 89d620da3d..8e0da8266a 100644 --- a/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs +++ b/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs @@ -987,7 +987,10 @@ private void RemoveMarkerLoadout() foreach (var loadout in _loadoutRegistry.AllLoadouts()) { if (loadout.IsMarkerLoadout) + { _loadoutRegistry.Delete(loadout.LoadoutId); + return; + } } } From c72c53a418c90c805a7d292c22112fad687d2efd Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Wed, 1 May 2024 05:02:03 +0100 Subject: [PATCH 06/19] Removed: Unused Using(s) --- .../ALoadoutSynchronizer.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs b/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs index 8e0da8266a..4dbe21904b 100644 --- a/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs +++ b/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs @@ -1,6 +1,5 @@ using System.Collections.Concurrent; using System.Diagnostics; -using System.Net.Security; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Microsoft.Extensions.DependencyInjection; @@ -21,7 +20,6 @@ using NexusMods.Abstractions.Loadouts.Visitors; using NexusMods.Abstractions.Serialization; using NexusMods.Abstractions.Serialization.DataModel; -using NexusMods.Abstractions.Serialization.DataModel.Ids; using NexusMods.Extensions.BCL; using NexusMods.Hashing.xxHash64; using NexusMods.Paths; From 2697a79ba398690e329abf876c68d94684ebf894 Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Wed, 1 May 2024 05:11:40 +0100 Subject: [PATCH 07/19] Cleaned up Marker Loadouts PR --- .../ALoadoutSynchronizer.cs | 5 +-- .../Loadout.cs | 3 +- .../Pages/MyGames/MyGamesViewModel.cs | 38 +++++++++---------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs b/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs index 4dbe21904b..9a4ee33842 100644 --- a/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs +++ b/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs @@ -590,8 +590,7 @@ public virtual async Task Apply(Loadout loadout, bool forceSkipIn var diskState = await FileTreeToDisk(fileTree, loadout, flattened, prevState, loadout.Installation, forceSkipIngest); diskState.LoadoutRevision = loadout.DataStoreId; await _diskStateRegistry.SaveState(loadout.Installation, diskState); - - // Remove an existing temporary marker loadout. + if (!loadout.IsMarkerLoadout) RemoveMarkerLoadout(); @@ -879,7 +878,7 @@ to its initial state and create a 'hidden' loadout to accomodate this. This is a good default for many cases: - - Game files are not likely to be overwritten, so this will + - Game files are not likely to be overwritten, so this will just end up materialising into a bunch of deletes. (Very Fast) - Ensures internal consistency. i.e. 'last applied loadout' is always diff --git a/src/Abstractions/NexusMods.Abstractions.Loadouts/Loadout.cs b/src/Abstractions/NexusMods.Abstractions.Loadouts/Loadout.cs index d88d44b527..d43de08eff 100644 --- a/src/Abstractions/NexusMods.Abstractions.Loadouts/Loadout.cs +++ b/src/Abstractions/NexusMods.Abstractions.Loadouts/Loadout.cs @@ -50,7 +50,8 @@ public record Loadout : Entity, IEmptyWithDataStore /// /// This is true if the loadout is a hidden 'Marker' loadout. /// A marker loadout is created from the original game state and should - /// be a singleton. + /// be a singleton for a given game. It is a temporary loadout that is + /// destroyed when a real loadout is applied. /// /// Marker loadouts should not be shown in any user facing elements. /// diff --git a/src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs b/src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs index 1b187de25d..6577b52013 100644 --- a/src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs +++ b/src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs @@ -137,27 +137,27 @@ private async Task ManageGame(GameInstallation installation) var marker = await _loadoutRegistry.Manage(installation, name); var loadoutId = marker.Id; - Dispatcher.UIThread.Invoke(() => { SwitchToLoadoutId(loadoutId); }); - } - - private void SwitchToLoadoutId(LoadoutId loadoutId) - { - if (!_windowManager.TryGetActiveWindow(out var window)) return; - var workspaceController = window.WorkspaceController; - workspaceController.ChangeOrCreateWorkspaceByContext( - context => context.LoadoutId == loadoutId, - () => new PageData - { - FactoryId = LoadoutGridPageFactory.StaticId, - Context = new LoadoutGridContext - { - LoadoutId = loadoutId - } - }, - () => new LoadoutContext + Dispatcher.UIThread.Invoke(() => { - LoadoutId = loadoutId + if (!_windowManager.TryGetActiveWindow(out var window)) return; + var workspaceController = window.WorkspaceController; + + workspaceController.ChangeOrCreateWorkspaceByContext( + context => context.LoadoutId == loadoutId, + () => new PageData + { + FactoryId = LoadoutGridPageFactory.StaticId, + Context = new LoadoutGridContext + { + LoadoutId = loadoutId + } + }, + () => new LoadoutContext + { + LoadoutId = loadoutId + } + ); } ); } From e8d915043966abf35dd0cf37064defb9c0190b91 Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Wed, 1 May 2024 15:09:58 +0100 Subject: [PATCH 08/19] Added: [Edge Case] Force Skip Ingest if Last Loadout was a Marker --- .../ALoadoutSynchronizer.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs b/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs index 9a4ee33842..6782dc9b47 100644 --- a/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs +++ b/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs @@ -584,6 +584,10 @@ public virtual Loadout MergeLoadouts(Loadout loadoutA, Loadout loadoutB) /// public virtual async Task Apply(Loadout loadout, bool forceSkipIngest = false) { + // Note(Sewer) If the last loadout was a marker loadout, we need to guard + // in the case the user has modified the game folder since. + forceSkipIngest = forceSkipIngest || IsLastLoadoutAMarkerLoadout(loadout.Installation); + var flattened = await LoadoutToFlattenedLoadout(loadout); var fileTree = await FlattenedLoadoutToFileTree(flattened, loadout); var prevState = _diskStateRegistry.GetState(loadout.Installation)!; @@ -970,8 +974,21 @@ protected virtual async Task> SortMods(Loadout loadout) } #endregion - #region Misc Helper Functions + /// + /// Checks if the last applied loadout is a 'marker' loadout. + /// + /// The game installation to check. + /// True if the last applied loadout is a marker loadout. + private bool IsLastLoadoutAMarkerLoadout(GameInstallation installation) + { + var lastId = _diskStateRegistry.GetLastAppliedLoadout(installation); + if (lastId == null) + return false; + + return _loadoutRegistry.GetLoadout(lastId) is { IsMarkerLoadout: true }; + } + /// /// Removes the first found 'marker' loadout from the data store. /// From b3e12db4d01ef17be3e3971059a30d5ba7745611 Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Wed, 1 May 2024 15:10:46 +0100 Subject: [PATCH 09/19] Improved: Make it more explicit in actual DataModel that the marker loadout is temporary. --- .../ALoadoutSynchronizer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs b/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs index 6782dc9b47..db80c9aa6b 100644 --- a/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs +++ b/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs @@ -926,7 +926,7 @@ private async Task CreateMarkerLoadout(GameInstallation installation) var loadout = _loadoutRegistry.Alter(loadoutId, "Marker Loadout", loadout => loadout with { - Name = "Marker Loadout", + Name = "Temporary Marker Loadout", Installation = installation, Mods = loadout.Mods.With(gameFiles.Id, gameFiles), IsMarkerLoadout = true, From 592ca24f8716391faf45175c644491c85a2bd96f Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Wed, 1 May 2024 15:29:31 +0100 Subject: [PATCH 10/19] Updated: Comment for detecting if Marker Loadout --- .../ALoadoutSynchronizer.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs b/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs index db80c9aa6b..ca32db409b 100644 --- a/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs +++ b/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs @@ -584,8 +584,9 @@ public virtual Loadout MergeLoadouts(Loadout loadoutA, Loadout loadoutB) /// public virtual async Task Apply(Loadout loadout, bool forceSkipIngest = false) { - // Note(Sewer) If the last loadout was a marker loadout, we need to guard - // in the case the user has modified the game folder since. + // Note(Sewer) If the last loadout was a marker loadout, we need to + // skip ingest and ignore changes, since marker loadouts should not be changed. + // (Prevent 'Needs Ingest' exception) forceSkipIngest = forceSkipIngest || IsLastLoadoutAMarkerLoadout(loadout.Installation); var flattened = await LoadoutToFlattenedLoadout(loadout); From 173886c13c3b699be1d9cb6bb123cb9f8358b3da Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Wed, 1 May 2024 15:37:36 +0100 Subject: [PATCH 11/19] Changed: Moved navigating away from current removed loadout to Spine --- .../Controls/Spine/SpineViewModel.cs | 18 ++++++++++++++++++ .../Loadout/LoadoutLeftMenuViewModel.cs | 17 ----------------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/NexusMods.App.UI/Controls/Spine/SpineViewModel.cs b/src/NexusMods.App.UI/Controls/Spine/SpineViewModel.cs index a89810c244..9362f63e7c 100644 --- a/src/NexusMods.App.UI/Controls/Spine/SpineViewModel.cs +++ b/src/NexusMods.App.UI/Controls/Spine/SpineViewModel.cs @@ -117,6 +117,24 @@ public SpineViewModel( .SubscribeWithErrorLogging() .DisposeWith(disposables); + // Navigate away from the Loadout workspace if the Loadout is removed + loadoutRegistry.LoadoutRootChanges + .OnUI() + .OnItemRemoved(loadoutId => + { + if (workspaceController.ActiveWorkspace?.Context is LoadoutContext activeLoadoutContext && + activeLoadoutContext.LoadoutId == loadoutId) + { + workspaceController.ChangeOrCreateWorkspaceByContext(() => new PageData + { + FactoryId = MyGamesPageFactory.StaticId, + Context = new MyGamesPageContext(), + }); + } + }, false) + .SubscribeWithErrorLogging() + .DisposeWith(disposables); + // Update the LeftMenuViewModel when the active workspace changes workspaceController.WhenAnyValue(controller => controller.ActiveWorkspace) .Select(workspace => workspace?.Id) diff --git a/src/NexusMods.App.UI/LeftMenu/Loadout/LoadoutLeftMenuViewModel.cs b/src/NexusMods.App.UI/LeftMenu/Loadout/LoadoutLeftMenuViewModel.cs index 1c91a5c37c..30d973e7d3 100644 --- a/src/NexusMods.App.UI/LeftMenu/Loadout/LoadoutLeftMenuViewModel.cs +++ b/src/NexusMods.App.UI/LeftMenu/Loadout/LoadoutLeftMenuViewModel.cs @@ -118,23 +118,6 @@ public LoadoutLeftMenuViewModel( diagnosticItem.Name = $"{Language.LoadoutLeftMenuViewModel_LoadoutLeftMenuViewModel_Diagnostics} ({totalCount})"; }) .DisposeWith(disposable); - - loadoutRegistry.LoadoutRootChanges - .OnUI() - .OnItemRemoved(loadoutId => - { - if (workspaceController.ActiveWorkspace?.Context is LoadoutContext activeLoadoutContext && - activeLoadoutContext.LoadoutId == loadoutId) - { - workspaceController.ChangeOrCreateWorkspaceByContext(() => new PageData - { - FactoryId = MyGamesPageFactory.StaticId, - Context = new MyGamesPageContext(), - }); - } - }, false) - .SubscribeWithErrorLogging() - .DisposeWith(disposable); }); } } From 6675d170fdf50feaf64a813c70d778e6746b97bc Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Wed, 1 May 2024 15:40:10 +0100 Subject: [PATCH 12/19] Changed: Explicit Unmanage Game if we're now Last Loadout --- .../ALoadoutSynchronizer.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs b/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs index ca32db409b..4a38891170 100644 --- a/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs +++ b/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs @@ -865,11 +865,17 @@ public async Task DeleteLoadout(GameInstallation installation, LoadoutId id) // Clear Initial State if this is the only loadout for the game. // We use folder location for this. var installLocation = installation.LocationsRegister[LocationId.Game].ToString(); - var wasLastLoadout = _loadoutRegistry + var isLastLoadout = _loadoutRegistry .AllLoadouts() .Count(x => x.Installation.LocationsRegister[LocationId.Game].ToString() == installLocation && !x.IsMarkerLoadout) <= 1; + + if (isLastLoadout) + { + await UnManage(installation); + return; + } if (_loadoutRegistry.IsApplied(id, _diskStateRegistry.GetLastAppliedLoadout(installation))) { @@ -905,8 +911,6 @@ a valid loadout. } _loadoutRegistry.Delete(id); - if (wasLastLoadout) - await _diskStateRegistry.ClearInitialState(installation); } /// From 0db551acbfc6f438c4bbfb835b62439429ff9551 Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Wed, 1 May 2024 16:12:06 +0100 Subject: [PATCH 13/19] Added: Navigate to First non-Marker Loadout if Last Applied is Marker --- src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs b/src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs index 6577b52013..9233d4b809 100644 --- a/src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs +++ b/src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs @@ -180,6 +180,13 @@ private void NavigateToLoadout(GameInstallation installation) _logger.LogError("Unable to find loadout for revision {RevId}", revId); return; } + + if (loadout.IsMarkerLoadout) + { + // Last loadout was a marker, we'll pick some non-marker loadout + // so the user can apply it. + loadout = _loadoutRegistry.AllLoadouts().First(x => !x.IsMarkerLoadout); + } var loadoutId = loadout.LoadoutId; From 21787a3a730f49fd4898a6a9b08a758c8427b0c4 Mon Sep 17 00:00:00 2001 From: erri120 Date: Thu, 2 May 2024 10:41:49 +0200 Subject: [PATCH 14/19] Use IWorkspaceContext.IsValid --- .../001-querying-windows-and-workspaces.cs | 7 ++- .../Windows/DesignWindowManager.cs | 2 +- .../Windows/IWindowManager.cs | 4 +- .../Windows/MainWindowViewModel.cs | 54 +------------------ src/NexusMods.App.UI/Windows/WindowManager.cs | 3 +- .../Context/DownloadsContext.cs | 5 +- .../WorkspaceSystem/Context/EmptyContext.cs | 2 + .../WorkspaceSystem/Context/HomeContext.cs | 5 +- .../Context/IWorkspaceContext.cs | 13 ++++- .../WorkspaceSystem/Context/LoadoutContext.cs | 7 +++ .../WorkspaceController.cs | 15 ++++-- 11 files changed, 51 insertions(+), 66 deletions(-) diff --git a/src/Examples/Workspaces/001-querying-windows-and-workspaces.cs b/src/Examples/Workspaces/001-querying-windows-and-workspaces.cs index 82f461e7b0..f2ed8172b6 100644 --- a/src/Examples/Workspaces/001-querying-windows-and-workspaces.cs +++ b/src/Examples/Workspaces/001-querying-windows-and-workspaces.cs @@ -50,7 +50,12 @@ public void Do() } } -file record ExampleContext : IWorkspaceContext; +file record ExampleContext : IWorkspaceContext +{ + // Use this method to verify if the data in the context is still valid. + // The workspace will be discarded if the data becomes invalid. + public bool IsValid(IServiceProvider serviceProvider) => true; +} // This example class shows how to query Windows and Workspaces from within a page. file class ExamplePage : APageViewModel, IMyPageInterface diff --git a/src/NexusMods.App.UI/Windows/DesignWindowManager.cs b/src/NexusMods.App.UI/Windows/DesignWindowManager.cs index dbe9d2a726..730c4ea112 100644 --- a/src/NexusMods.App.UI/Windows/DesignWindowManager.cs +++ b/src/NexusMods.App.UI/Windows/DesignWindowManager.cs @@ -29,5 +29,5 @@ public void RegisterWindow(IWorkspaceWindow window) { } public void UnregisterWindow(IWorkspaceWindow window) { } public void SaveWindowState(IWorkspaceWindow window) { } - public bool RestoreWindowState(IWorkspaceWindow window, Func? sanitize = null) => false; + public bool RestoreWindowState(IWorkspaceWindow window) => false; } diff --git a/src/NexusMods.App.UI/Windows/IWindowManager.cs b/src/NexusMods.App.UI/Windows/IWindowManager.cs index 331ece8e9a..8f1614edd7 100644 --- a/src/NexusMods.App.UI/Windows/IWindowManager.cs +++ b/src/NexusMods.App.UI/Windows/IWindowManager.cs @@ -1,7 +1,6 @@ using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using JetBrains.Annotations; -using NexusMods.App.UI.WorkspaceSystem; namespace NexusMods.App.UI.Windows; @@ -54,7 +53,6 @@ public interface IWindowManager /// Restores the saved window state. /// /// - /// Optional method to sanitize the restored data. /// Whether the restore was successful. - public bool RestoreWindowState(IWorkspaceWindow window, Func? sanitize = null); + public bool RestoreWindowState(IWorkspaceWindow window); } diff --git a/src/NexusMods.App.UI/Windows/MainWindowViewModel.cs b/src/NexusMods.App.UI/Windows/MainWindowViewModel.cs index 5b5c14b369..bd763a0f59 100644 --- a/src/NexusMods.App.UI/Windows/MainWindowViewModel.cs +++ b/src/NexusMods.App.UI/Windows/MainWindowViewModel.cs @@ -112,65 +112,13 @@ public MainWindowViewModel( vm._windowManager.UnregisterWindow(vm); }).DisposeWith(d); - if (!_windowManager.RestoreWindowState(this, SanitizeWindowData)) + if (!_windowManager.RestoreWindowState(this)) { // NOTE(erri120): select home on startup if we didn't restore the previous state Spine.NavigateToHome(); } }); } - - private WindowData SanitizeWindowData(WindowData data) - { - /* - Note(Sewer) - - Some of our persisted workspaces are dependent on the contents of the - datastore. For example, we create a workspace for each loadout. - - In some rare scenarios, it may however become possible that our - saved window state is not valid. - - For loadouts, some examples would be: - - - User deleted a loadout from the CLI - - Which may not remove the workspace. - - App crashes before complete removal of a loadout is complete. - - Although we could reorder operations, or add explicit code to update - the current workspaces from say, the CLI command; there's still - some inherent risks. - - Suppose someone writes some code that could run before the UI is - fully set up. They would have to remember to update workspaces. - If they forget, there's a chance of creating a potentially - very silent, hard to find bug. - - Therefore, we sanitize the data here, doing cleanup on no longer - valid items. - */ - - var workspaces = new List(data.Workspaces.Length); - var activeWorkspaceId = data.ActiveWorkspaceId; - foreach (var workspace in data.Workspaces) - { - if (workspace.Context is LoadoutContext loadout && !_registry.Contains(loadout.LoadoutId)) - { - if (activeWorkspaceId == workspace.Id) - activeWorkspaceId = null; - - continue; - } - - workspaces.Add(workspace); - } - - return data with - { - Workspaces = workspaces.ToArray(), - ActiveWorkspaceId = activeWorkspaceId, - }; - } internal void OnClose() { diff --git a/src/NexusMods.App.UI/Windows/WindowManager.cs b/src/NexusMods.App.UI/Windows/WindowManager.cs index 72f1a67ab8..f65dfaf527 100644 --- a/src/NexusMods.App.UI/Windows/WindowManager.cs +++ b/src/NexusMods.App.UI/Windows/WindowManager.cs @@ -94,14 +94,13 @@ public void SaveWindowState(IWorkspaceWindow window) } } - public bool RestoreWindowState(IWorkspaceWindow window, Func? sanitize) + public bool RestoreWindowState(IWorkspaceWindow window) { try { var data = _dataStore.Get(WindowData.Id); if (data is null) return false; - data = sanitize != null ? sanitize(data) : data; window.WorkspaceController.FromData(data); return true; } diff --git a/src/NexusMods.App.UI/WorkspaceSystem/Context/DownloadsContext.cs b/src/NexusMods.App.UI/WorkspaceSystem/Context/DownloadsContext.cs index 6a90fa6ab1..49a1bd41c8 100644 --- a/src/NexusMods.App.UI/WorkspaceSystem/Context/DownloadsContext.cs +++ b/src/NexusMods.App.UI/WorkspaceSystem/Context/DownloadsContext.cs @@ -3,4 +3,7 @@ namespace NexusMods.App.UI.WorkspaceSystem; [JsonName("NexusMods.App.UI.WorkspaceSystem.DownloadsContext")] -public record DownloadsContext : IWorkspaceContext; +public record DownloadsContext : IWorkspaceContext +{ + public bool IsValid(IServiceProvider serviceProvider) => true; +} diff --git a/src/NexusMods.App.UI/WorkspaceSystem/Context/EmptyContext.cs b/src/NexusMods.App.UI/WorkspaceSystem/Context/EmptyContext.cs index 984d665c97..d46ddddf3c 100644 --- a/src/NexusMods.App.UI/WorkspaceSystem/Context/EmptyContext.cs +++ b/src/NexusMods.App.UI/WorkspaceSystem/Context/EmptyContext.cs @@ -6,4 +6,6 @@ namespace NexusMods.App.UI.WorkspaceSystem; public record EmptyContext : IWorkspaceContext { public static readonly IWorkspaceContext Instance = new EmptyContext(); + + public bool IsValid(IServiceProvider serviceProvider) => true; } diff --git a/src/NexusMods.App.UI/WorkspaceSystem/Context/HomeContext.cs b/src/NexusMods.App.UI/WorkspaceSystem/Context/HomeContext.cs index 2bf1c62cca..2ccdedf2cb 100644 --- a/src/NexusMods.App.UI/WorkspaceSystem/Context/HomeContext.cs +++ b/src/NexusMods.App.UI/WorkspaceSystem/Context/HomeContext.cs @@ -3,4 +3,7 @@ namespace NexusMods.App.UI.WorkspaceSystem; [JsonName("NexusMods.App.UI.WorkspaceSystem.HomeContext")] -public record HomeContext : IWorkspaceContext; +public record HomeContext : IWorkspaceContext +{ + public bool IsValid(IServiceProvider serviceProvider) => true; +} diff --git a/src/NexusMods.App.UI/WorkspaceSystem/Context/IWorkspaceContext.cs b/src/NexusMods.App.UI/WorkspaceSystem/Context/IWorkspaceContext.cs index e87a60ff04..51fb3af5ec 100644 --- a/src/NexusMods.App.UI/WorkspaceSystem/Context/IWorkspaceContext.cs +++ b/src/NexusMods.App.UI/WorkspaceSystem/Context/IWorkspaceContext.cs @@ -2,5 +2,16 @@ namespace NexusMods.App.UI.WorkspaceSystem; +/// +/// Represents a workspace context. +/// [PublicAPI] -public interface IWorkspaceContext; +public interface IWorkspaceContext +{ + /// + /// Returns true if the current workspace context is still valid, meaning + /// the data inside the context is still available and isn't referring to + /// missing or deleted data. + /// + public bool IsValid(IServiceProvider serviceProvider); +} diff --git a/src/NexusMods.App.UI/WorkspaceSystem/Context/LoadoutContext.cs b/src/NexusMods.App.UI/WorkspaceSystem/Context/LoadoutContext.cs index 0ad954a8eb..ba7645942c 100644 --- a/src/NexusMods.App.UI/WorkspaceSystem/Context/LoadoutContext.cs +++ b/src/NexusMods.App.UI/WorkspaceSystem/Context/LoadoutContext.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.DependencyInjection; using NexusMods.Abstractions.Loadouts; using NexusMods.Abstractions.Serialization.Attributes; @@ -7,4 +8,10 @@ namespace NexusMods.App.UI.WorkspaceSystem; public record LoadoutContext : IWorkspaceContext { public required LoadoutId LoadoutId { get; init; } + + public bool IsValid(IServiceProvider serviceProvider) + { + var loadoutRegistry = serviceProvider.GetRequiredService(); + return loadoutRegistry.Contains(LoadoutId); + } } diff --git a/src/NexusMods.App.UI/WorkspaceSystem/WorkspaceController/WorkspaceController.cs b/src/NexusMods.App.UI/WorkspaceSystem/WorkspaceController/WorkspaceController.cs index b98307344c..f769e860d8 100644 --- a/src/NexusMods.App.UI/WorkspaceSystem/WorkspaceController/WorkspaceController.cs +++ b/src/NexusMods.App.UI/WorkspaceSystem/WorkspaceController/WorkspaceController.cs @@ -21,6 +21,7 @@ namespace NexusMods.App.UI.WorkspaceSystem; [UsedImplicitly] internal sealed class WorkspaceController : ReactiveObject, IWorkspaceController { + private readonly IServiceProvider _serviceProvider; private readonly IWorkspaceWindow _window; private readonly ILogger _logger; private readonly IWorkspaceAttachmentsFactoryManager _workspaceAttachmentsFactory; @@ -36,6 +37,7 @@ internal sealed class WorkspaceController : ReactiveObject, IWorkspaceController public WorkspaceController(IWorkspaceWindow window, IServiceProvider serviceProvider) { + _serviceProvider = serviceProvider; _window = window; _logger = serviceProvider.GetRequiredService>(); @@ -71,15 +73,22 @@ public void FromData(WindowData data) foreach (var workspaceData in data.Workspaces) { + var isActiveWorkspace = workspaceData.Id == data.ActiveWorkspaceId; + + var context = workspaceData.Context; + if (!context.IsValid(_serviceProvider)) + { + _logger.LogWarning("Workspace with Context {Context} ({Type}) is no longer valid and has been removed", context, context.GetType()); + continue; + } + var vm = CreateWorkspace( context: Optional.Create(workspaceData.Context), pageData: Optional.None ); vm.FromData(workspaceData); - - if (workspaceData.Id == data.ActiveWorkspaceId) - activeWorkspace = vm; + if (isActiveWorkspace) activeWorkspace = vm; } ChangeActiveWorkspace(activeWorkspace?.Id ?? _workspaces.Keys.First()); From cc9c025e669bb19a877a71f289de04076ec1cb07 Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Thu, 2 May 2024 15:12:59 +0100 Subject: [PATCH 15/19] Reworked slightly how 'Marker Loadout(s)' are stored internally; making code better forward compatible. --- .../ALoadoutSynchronizer.cs | 10 ++++----- .../Loadout.cs | 14 ++++++++++++- .../LoadoutKind.cs | 21 +++++++++++++++++++ .../Controls/Spine/SpineViewModel.cs | 2 +- .../Pages/MyGames/MyGamesViewModel.cs | 8 +++---- .../Verbs/LoadoutManagementVerbs.cs | 2 +- 6 files changed, 44 insertions(+), 13 deletions(-) create mode 100644 src/Abstractions/NexusMods.Abstractions.Loadouts/LoadoutKind.cs diff --git a/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs b/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs index 4a38891170..a92a8880f4 100644 --- a/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs +++ b/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs @@ -934,7 +934,7 @@ private async Task CreateMarkerLoadout(GameInstallation installation) Name = "Temporary Marker Loadout", Installation = installation, Mods = loadout.Mods.With(gameFiles.Id, gameFiles), - IsMarkerLoadout = true, + LoadoutKind = LoadoutKind.Marker, }); return loadout; @@ -1005,11 +1005,9 @@ private void RemoveMarkerLoadout() { foreach (var loadout in _loadoutRegistry.AllLoadouts()) { - if (loadout.IsMarkerLoadout) - { - _loadoutRegistry.Delete(loadout.LoadoutId); - return; - } + if (!loadout.IsMarkerLoadout) continue; + _loadoutRegistry.Delete(loadout.LoadoutId); + return; } } diff --git a/src/Abstractions/NexusMods.Abstractions.Loadouts/Loadout.cs b/src/Abstractions/NexusMods.Abstractions.Loadouts/Loadout.cs index d43de08eff..35418e9de0 100644 --- a/src/Abstractions/NexusMods.Abstractions.Loadouts/Loadout.cs +++ b/src/Abstractions/NexusMods.Abstractions.Loadouts/Loadout.cs @@ -47,6 +47,11 @@ public record Loadout : Entity, IEmptyWithDataStore /// public required EntityLink PreviousVersion { get; init; } + /// + /// Specifies the type of the loadout that the current loadout represents + /// + public LoadoutKind LoadoutKind { get; init; } + /// /// This is true if the loadout is a hidden 'Marker' loadout. /// A marker loadout is created from the original game state and should @@ -55,7 +60,14 @@ public record Loadout : Entity, IEmptyWithDataStore /// /// Marker loadouts should not be shown in any user facing elements. /// - public bool IsMarkerLoadout { get; init; } + public bool IsMarkerLoadout => LoadoutKind == LoadoutKind.Marker; + + /// + /// Returns true if the loadout should be visible to the user. + /// + public bool IsVisible => LoadoutKind == LoadoutKind.Default; + // Note(sewer), it's better to 'opt into' functionality, than opt out. + // especially, when it comes to displaying elements the user can edit. /// public override EntityCategory Category => EntityCategory.Loadouts; diff --git a/src/Abstractions/NexusMods.Abstractions.Loadouts/LoadoutKind.cs b/src/Abstractions/NexusMods.Abstractions.Loadouts/LoadoutKind.cs new file mode 100644 index 0000000000..3fb30cfd12 --- /dev/null +++ b/src/Abstractions/NexusMods.Abstractions.Loadouts/LoadoutKind.cs @@ -0,0 +1,21 @@ +namespace NexusMods.Abstractions.Loadouts; + +/// +/// Specifies the 'type' of loadout that the current loadout represents. +/// +public enum LoadoutKind : byte +{ + /// + /// This is a regular loadout that's created by the user + /// + Default, + + /// + /// This is a temporary loadout that is created from the original game state + /// upon removal of the currently active loadout. This loadout is removed + /// when a 'real' loadout is applied. + /// + /// A GameInstallation should only have 1 marker loadout. + /// + Marker, +} diff --git a/src/NexusMods.App.UI/Controls/Spine/SpineViewModel.cs b/src/NexusMods.App.UI/Controls/Spine/SpineViewModel.cs index 9362f63e7c..a4ae755d0a 100644 --- a/src/NexusMods.App.UI/Controls/Spine/SpineViewModel.cs +++ b/src/NexusMods.App.UI/Controls/Spine/SpineViewModel.cs @@ -77,7 +77,7 @@ public SpineViewModel( { loadoutRegistry.LoadoutRootChanges .Transform(loadoutId => (loadoutId, loadout: loadoutRegistry.Get(loadoutId))) - .Filter(tuple => tuple.loadout is { IsMarkerLoadout: false }) + .Filter(tuple => tuple.loadout is { IsVisible: true }) .TransformAsync(async tuple => { var loadoutId = tuple.loadoutId; diff --git a/src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs b/src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs index 9233d4b809..6e6a7aa3a0 100644 --- a/src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs +++ b/src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs @@ -181,11 +181,11 @@ private void NavigateToLoadout(GameInstallation installation) return; } - if (loadout.IsMarkerLoadout) + if (!loadout.IsVisible) { - // Last loadout was a marker, we'll pick some non-marker loadout - // so the user can apply it. - loadout = _loadoutRegistry.AllLoadouts().First(x => !x.IsMarkerLoadout); + // Last loadout was most likely a marker, we'll pick some loadout + // the user is supposed to see so the user can apply it. + loadout = _loadoutRegistry.AllLoadouts().First(x => x.IsVisible); } var loadoutId = loadout.LoadoutId; diff --git a/src/NexusMods.DataModel/CommandLine/Verbs/LoadoutManagementVerbs.cs b/src/NexusMods.DataModel/CommandLine/Verbs/LoadoutManagementVerbs.cs index 63f383fca7..dcaf5f265b 100644 --- a/src/NexusMods.DataModel/CommandLine/Verbs/LoadoutManagementVerbs.cs +++ b/src/NexusMods.DataModel/CommandLine/Verbs/LoadoutManagementVerbs.cs @@ -148,7 +148,7 @@ private static async Task ListLoadouts([Injected] IRenderer renderer, [Injected] CancellationToken token) { var rows = registry.AllLoadouts() - .Where(x => !x.IsMarkerLoadout) + .Where(x => x.IsVisible) .Select(list => new object[] { list.Name, list.Installation, list.LoadoutId, list.Mods.Count }) .ToList(); From 8a38e5984228f30ffb5e378ca8ef33c55edc935b Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Thu, 2 May 2024 15:19:55 +0100 Subject: [PATCH 16/19] Added: Note on why we sanitize LoadoutContext. --- .../WorkspaceSystem/Context/LoadoutContext.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/NexusMods.App.UI/WorkspaceSystem/Context/LoadoutContext.cs b/src/NexusMods.App.UI/WorkspaceSystem/Context/LoadoutContext.cs index ba7645942c..e649690811 100644 --- a/src/NexusMods.App.UI/WorkspaceSystem/Context/LoadoutContext.cs +++ b/src/NexusMods.App.UI/WorkspaceSystem/Context/LoadoutContext.cs @@ -11,6 +11,18 @@ public record LoadoutContext : IWorkspaceContext public bool IsValid(IServiceProvider serviceProvider) { + /* + Note(Sewer) + + In App, we create a loadout workspace for each loadout. + + It may be possible that this loadout no longer exists, as a result + of removal from the CLI (UI not running) or a crash during removal. + [i.e. in cases we may have not run explicit workspace delete] + + In such cases, we may need to discard invalid loadouts, which we + do here. + */ var loadoutRegistry = serviceProvider.GetRequiredService(); return loadoutRegistry.Contains(LoadoutId); } From 6a59520e8040fe03c0e04a9247bd1a919ccd9e3d Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Thu, 2 May 2024 15:25:00 +0100 Subject: [PATCH 17/19] Changed: Be More Explicit in why we do the visibility check in View Button on MyGames --- src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs b/src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs index 6e6a7aa3a0..bac855b960 100644 --- a/src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs +++ b/src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs @@ -181,10 +181,10 @@ private void NavigateToLoadout(GameInstallation installation) return; } + // We can't navigate to an invisible loadout, make sure we pick a visible one. if (!loadout.IsVisible) { - // Last loadout was most likely a marker, we'll pick some loadout - // the user is supposed to see so the user can apply it. + // Note(sewer) | If we're here, last loadout was most likely a marker loadout = _loadoutRegistry.AllLoadouts().First(x => x.IsVisible); } From cd3e7d7b3f389402d3e67374adfccbe71a373656 Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Thu, 2 May 2024 15:33:54 +0100 Subject: [PATCH 18/19] Refactor: IsMarkerLoadout and IsVisible to Methods --- .../ALoadoutSynchronizer.cs | 9 ++--- .../Loadout.cs | 36 ++++++++++--------- .../Controls/Spine/SpineViewModel.cs | 2 +- .../Pages/MyGames/MyGamesViewModel.cs | 4 +-- .../Verbs/LoadoutManagementVerbs.cs | 2 +- 5 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs b/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs index a92a8880f4..97cf18c429 100644 --- a/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs +++ b/src/Abstractions/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs @@ -596,7 +596,7 @@ public virtual async Task Apply(Loadout loadout, bool forceSkipIn diskState.LoadoutRevision = loadout.DataStoreId; await _diskStateRegistry.SaveState(loadout.Installation, diskState); - if (!loadout.IsMarkerLoadout) + if (!loadout.IsMarkerLoadout()) RemoveMarkerLoadout(); return diskState; @@ -869,7 +869,7 @@ public async Task DeleteLoadout(GameInstallation installation, LoadoutId id) .AllLoadouts() .Count(x => x.Installation.LocationsRegister[LocationId.Game].ToString() == installLocation - && !x.IsMarkerLoadout) <= 1; + && !x.IsMarkerLoadout()) <= 1; if (isLastLoadout) { @@ -991,7 +991,8 @@ private bool IsLastLoadoutAMarkerLoadout(GameInstallation installation) if (lastId == null) return false; - return _loadoutRegistry.GetLoadout(lastId) is { IsMarkerLoadout: true }; + var loadout = _loadoutRegistry.GetLoadout(lastId); + return loadout != null && loadout.IsMarkerLoadout(); } /// @@ -1005,7 +1006,7 @@ private void RemoveMarkerLoadout() { foreach (var loadout in _loadoutRegistry.AllLoadouts()) { - if (!loadout.IsMarkerLoadout) continue; + if (!loadout.IsMarkerLoadout()) continue; _loadoutRegistry.Delete(loadout.LoadoutId); return; } diff --git a/src/Abstractions/NexusMods.Abstractions.Loadouts/Loadout.cs b/src/Abstractions/NexusMods.Abstractions.Loadouts/Loadout.cs index 35418e9de0..2299b1f0af 100644 --- a/src/Abstractions/NexusMods.Abstractions.Loadouts/Loadout.cs +++ b/src/Abstractions/NexusMods.Abstractions.Loadouts/Loadout.cs @@ -52,23 +52,6 @@ public record Loadout : Entity, IEmptyWithDataStore /// public LoadoutKind LoadoutKind { get; init; } - /// - /// This is true if the loadout is a hidden 'Marker' loadout. - /// A marker loadout is created from the original game state and should - /// be a singleton for a given game. It is a temporary loadout that is - /// destroyed when a real loadout is applied. - /// - /// Marker loadouts should not be shown in any user facing elements. - /// - public bool IsMarkerLoadout => LoadoutKind == LoadoutKind.Marker; - - /// - /// Returns true if the loadout should be visible to the user. - /// - public bool IsVisible => LoadoutKind == LoadoutKind.Default; - // Note(sewer), it's better to 'opt into' functionality, than opt out. - // especially, when it comes to displaying elements the user can edit. - /// public override EntityCategory Category => EntityCategory.Loadouts; @@ -145,4 +128,23 @@ public Loadout AlterFiles(Func func) }) }; } + + /// + /// This is true if the loadout is a hidden 'Marker' loadout. + /// A marker loadout is created from the original game state and should + /// be a singleton for a given game. It is a temporary loadout that is + /// destroyed when a real loadout is applied. + /// + /// Marker loadouts should not be shown in any user facing elements. + /// + public bool IsMarkerLoadout() => LoadoutKind == LoadoutKind.Marker; + + /// + /// Returns true if the loadout should be visible to the user. + /// + /// + /// Note(sewer), it's better to 'opt into' functionality, than opt out. + /// especially, when it comes to displaying elements the user can edit. + /// + public bool IsVisible() => LoadoutKind == LoadoutKind.Default; } diff --git a/src/NexusMods.App.UI/Controls/Spine/SpineViewModel.cs b/src/NexusMods.App.UI/Controls/Spine/SpineViewModel.cs index a4ae755d0a..70e8ab866c 100644 --- a/src/NexusMods.App.UI/Controls/Spine/SpineViewModel.cs +++ b/src/NexusMods.App.UI/Controls/Spine/SpineViewModel.cs @@ -77,7 +77,7 @@ public SpineViewModel( { loadoutRegistry.LoadoutRootChanges .Transform(loadoutId => (loadoutId, loadout: loadoutRegistry.Get(loadoutId))) - .Filter(tuple => tuple.loadout is { IsVisible: true }) + .Filter(tuple => tuple.loadout != null && tuple.loadout.IsVisible()) .TransformAsync(async tuple => { var loadoutId = tuple.loadoutId; diff --git a/src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs b/src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs index bac855b960..0a0f8a5d17 100644 --- a/src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs +++ b/src/NexusMods.App.UI/Pages/MyGames/MyGamesViewModel.cs @@ -182,10 +182,10 @@ private void NavigateToLoadout(GameInstallation installation) } // We can't navigate to an invisible loadout, make sure we pick a visible one. - if (!loadout.IsVisible) + if (!loadout.IsVisible()) { // Note(sewer) | If we're here, last loadout was most likely a marker - loadout = _loadoutRegistry.AllLoadouts().First(x => x.IsVisible); + loadout = _loadoutRegistry.AllLoadouts().First(x => x.IsVisible()); } var loadoutId = loadout.LoadoutId; diff --git a/src/NexusMods.DataModel/CommandLine/Verbs/LoadoutManagementVerbs.cs b/src/NexusMods.DataModel/CommandLine/Verbs/LoadoutManagementVerbs.cs index dcaf5f265b..74592bc304 100644 --- a/src/NexusMods.DataModel/CommandLine/Verbs/LoadoutManagementVerbs.cs +++ b/src/NexusMods.DataModel/CommandLine/Verbs/LoadoutManagementVerbs.cs @@ -148,7 +148,7 @@ private static async Task ListLoadouts([Injected] IRenderer renderer, [Injected] CancellationToken token) { var rows = registry.AllLoadouts() - .Where(x => x.IsVisible) + .Where(x => x.IsVisible()) .Select(list => new object[] { list.Name, list.Installation, list.LoadoutId, list.Mods.Count }) .ToList(); From 2898003ba443771e476e9d01ef480916009b3f31 Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Thu, 2 May 2024 15:54:44 +0100 Subject: [PATCH 19/19] Revert "Added: Note on why we sanitize LoadoutContext." This reverts commit 8a38e5984228f30ffb5e378ca8ef33c55edc935b. --- .../WorkspaceSystem/Context/LoadoutContext.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/NexusMods.App.UI/WorkspaceSystem/Context/LoadoutContext.cs b/src/NexusMods.App.UI/WorkspaceSystem/Context/LoadoutContext.cs index e649690811..ba7645942c 100644 --- a/src/NexusMods.App.UI/WorkspaceSystem/Context/LoadoutContext.cs +++ b/src/NexusMods.App.UI/WorkspaceSystem/Context/LoadoutContext.cs @@ -11,18 +11,6 @@ public record LoadoutContext : IWorkspaceContext public bool IsValid(IServiceProvider serviceProvider) { - /* - Note(Sewer) - - In App, we create a loadout workspace for each loadout. - - It may be possible that this loadout no longer exists, as a result - of removal from the CLI (UI not running) or a crash during removal. - [i.e. in cases we may have not run explicit workspace delete] - - In such cases, we may need to discard invalid loadouts, which we - do here. - */ var loadoutRegistry = serviceProvider.GetRequiredService(); return loadoutRegistry.Contains(LoadoutId); }