diff --git a/NexusMods.App.sln b/NexusMods.App.sln index 5086507845..53661c13bd 100644 --- a/NexusMods.App.sln +++ b/NexusMods.App.sln @@ -214,7 +214,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Interprocess", "Interproces EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Settings.Tests", "tests\NexusMods.Settings.Tests\NexusMods.Settings.Tests.csproj", "{0D289DCE-1B17-4B63-B8B3-47CB852BF5B4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Abstractions.Library", "src\Abstractions\NexusMods.Abstractions.Library\NexusMods.Abstractions.Library.csproj", "{0044D340-E435-489C-A425-139AAB2EA205}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Abstractions.Library.Models", "src\Abstractions\NexusMods.Abstractions.Library.Models\NexusMods.Abstractions.Library.Models.csproj", "{0044D340-E435-489C-A425-139AAB2EA205}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Abstractions.NexusModsLibrary", "src\Abstractions\NexusMods.Abstractions.NexusModsLibrary\NexusMods.Abstractions.NexusModsLibrary.csproj", "{5D85EBB2-755F-4148-BFC4-8D2245A3105B}" EndProject @@ -228,6 +228,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Abstractions.Jobs EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Jobs.Tests", "tests\NexusMods.Jobs.Tests\NexusMods.Jobs.Tests.csproj", "{01043F6A-121B-4B3C-A694-B823D9CD0BB0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Abstractions.Library", "src\Abstractions\NexusMods.Abstractions.Library\NexusMods.Abstractions.Library.csproj", "{C47C59F4-1C6C-4F78-9A27-F328F1C02BF0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Jobs", "src\NexusMods.Jobs\NexusMods.Jobs.csproj", "{44E6BD8A-7A82-49CC-91FA-AF3B3E5FBEE9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Abstractions.Library.Installers", "src\Abstractions\NexusMods.Abstractions.Library.Installers\NexusMods.Abstractions.Library.Installers.csproj", "{F6482055-698C-492A-9FC2-0FCDC9FC2E23}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -586,6 +592,18 @@ Global {01043F6A-121B-4B3C-A694-B823D9CD0BB0}.Debug|Any CPU.Build.0 = Debug|Any CPU {01043F6A-121B-4B3C-A694-B823D9CD0BB0}.Release|Any CPU.ActiveCfg = Release|Any CPU {01043F6A-121B-4B3C-A694-B823D9CD0BB0}.Release|Any CPU.Build.0 = Release|Any CPU + {C47C59F4-1C6C-4F78-9A27-F328F1C02BF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C47C59F4-1C6C-4F78-9A27-F328F1C02BF0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C47C59F4-1C6C-4F78-9A27-F328F1C02BF0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C47C59F4-1C6C-4F78-9A27-F328F1C02BF0}.Release|Any CPU.Build.0 = Release|Any CPU + {44E6BD8A-7A82-49CC-91FA-AF3B3E5FBEE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44E6BD8A-7A82-49CC-91FA-AF3B3E5FBEE9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44E6BD8A-7A82-49CC-91FA-AF3B3E5FBEE9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44E6BD8A-7A82-49CC-91FA-AF3B3E5FBEE9}.Release|Any CPU.Build.0 = Release|Any CPU + {F6482055-698C-492A-9FC2-0FCDC9FC2E23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F6482055-698C-492A-9FC2-0FCDC9FC2E23}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F6482055-698C-492A-9FC2-0FCDC9FC2E23}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F6482055-698C-492A-9FC2-0FCDC9FC2E23}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -690,6 +708,9 @@ Global {821C1DA9-040E-44F3-BFCA-BF026C3F254B} = {0CB73565-1207-4A56-A79F-6A8E9BBD795C} {D15413CA-E727-40DA-8CD8-29ED052BF427} = {0CB73565-1207-4A56-A79F-6A8E9BBD795C} {01043F6A-121B-4B3C-A694-B823D9CD0BB0} = {52AF9D62-7D5B-4AD0-BA12-86F2AA67428B} + {C47C59F4-1C6C-4F78-9A27-F328F1C02BF0} = {0CB73565-1207-4A56-A79F-6A8E9BBD795C} + {44E6BD8A-7A82-49CC-91FA-AF3B3E5FBEE9} = {E7BAE287-D505-4D6D-A090-665A64309B2D} + {F6482055-698C-492A-9FC2-0FCDC9FC2E23} = {0CB73565-1207-4A56-A79F-6A8E9BBD795C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9F9F8352-34DD-42C0-8564-EE9AF34A3501} diff --git a/src/Abstractions/NexusMods.Abstractions.Games/AGame.cs b/src/Abstractions/NexusMods.Abstractions.Games/AGame.cs index 9f3ad2b107..1263b63f28 100644 --- a/src/Abstractions/NexusMods.Abstractions.Games/AGame.cs +++ b/src/Abstractions/NexusMods.Abstractions.Games/AGame.cs @@ -7,6 +7,7 @@ using NexusMods.Abstractions.Games.Loadouts; using NexusMods.Abstractions.Installers; using NexusMods.Abstractions.IO; +using NexusMods.Abstractions.Library.Installers; using NexusMods.Abstractions.Loadouts.Mods; using NexusMods.Abstractions.Loadouts.Synchronizers; using NexusMods.Abstractions.Serialization; @@ -70,6 +71,9 @@ protected virtual ILoadoutSynchronizer MakeSynchronizer(IServiceProvider provide /// public virtual IEnumerable Installers => _installers.Value; + /// + public virtual ILibraryItemInstaller[] LibraryItemInstallers { get; } = []; + /// public virtual IDiagnosticEmitter[] DiagnosticEmitters { get; } = Array.Empty(); diff --git a/src/Abstractions/NexusMods.Abstractions.Games/IGame.cs b/src/Abstractions/NexusMods.Abstractions.Games/IGame.cs index 5edec419d5..92023f7625 100644 --- a/src/Abstractions/NexusMods.Abstractions.Games/IGame.cs +++ b/src/Abstractions/NexusMods.Abstractions.Games/IGame.cs @@ -2,6 +2,7 @@ using NexusMods.Abstractions.GameLocators; using NexusMods.Abstractions.Installers; using NexusMods.Abstractions.IO; +using NexusMods.Abstractions.Library.Installers; using NexusMods.Abstractions.Loadouts.Mods; using NexusMods.Abstractions.Loadouts.Synchronizers; using NexusMods.Abstractions.Serialization; @@ -31,6 +32,11 @@ public interface IGame : ILocatableGame /// public IEnumerable Installers { get; } + /// + /// Gets all available installers this game supports. + /// + public ILibraryItemInstaller[] LibraryItemInstallers { get; } + /// /// An array of all instances of supported /// by the game. diff --git a/src/Abstractions/NexusMods.Abstractions.Games/NexusMods.Abstractions.Games.csproj b/src/Abstractions/NexusMods.Abstractions.Games/NexusMods.Abstractions.Games.csproj index d16acbb31a..161b992b82 100644 --- a/src/Abstractions/NexusMods.Abstractions.Games/NexusMods.Abstractions.Games.csproj +++ b/src/Abstractions/NexusMods.Abstractions.Games/NexusMods.Abstractions.Games.csproj @@ -24,6 +24,7 @@ + diff --git a/src/Abstractions/NexusMods.Abstractions.Installers/Extensions.cs b/src/Abstractions/NexusMods.Abstractions.Installers/Extensions.cs index 2f4851e77d..c899a23a7e 100644 --- a/src/Abstractions/NexusMods.Abstractions.Installers/Extensions.cs +++ b/src/Abstractions/NexusMods.Abstractions.Installers/Extensions.cs @@ -1,9 +1,6 @@ -using System.Collections.Immutable; using NexusMods.Abstractions.FileStore.Trees; using NexusMods.Abstractions.GameLocators; using NexusMods.Abstractions.Loadouts.Files; -using NexusMods.Abstractions.Loadouts.Mods; -using NexusMods.Abstractions.Serialization; using NexusMods.MnemonicDB.Abstractions.Models; using NexusMods.Paths; using NexusMods.Paths.Trees; diff --git a/src/Abstractions/NexusMods.Abstractions.Installers/IArchiveInstaller.cs b/src/Abstractions/NexusMods.Abstractions.Installers/IArchiveInstaller.cs index c7a1a044df..b7c6dba0ad 100644 --- a/src/Abstractions/NexusMods.Abstractions.Installers/IArchiveInstaller.cs +++ b/src/Abstractions/NexusMods.Abstractions.Installers/IArchiveInstaller.cs @@ -1,7 +1,6 @@ using NexusMods.Abstractions.Activities; using NexusMods.Abstractions.FileStore.Downloads; using NexusMods.Abstractions.Loadouts; -using NexusMods.Abstractions.Loadouts.Ids; using NexusMods.Abstractions.Loadouts.Mods; namespace NexusMods.Abstractions.Installers; diff --git a/src/Abstractions/NexusMods.Abstractions.Installers/ModInstallerInfo.cs b/src/Abstractions/NexusMods.Abstractions.Installers/ModInstallerInfo.cs index 0708b0bd80..baa7ad4b11 100644 --- a/src/Abstractions/NexusMods.Abstractions.Installers/ModInstallerInfo.cs +++ b/src/Abstractions/NexusMods.Abstractions.Installers/ModInstallerInfo.cs @@ -1,8 +1,6 @@ using JetBrains.Annotations; -using NexusMods.Abstractions.FileStore.ArchiveMetadata; using NexusMods.Abstractions.FileStore.Downloads; using NexusMods.Abstractions.GameLocators; -using NexusMods.Abstractions.Loadouts.Ids; using NexusMods.Abstractions.Loadouts.Mods; using ModFileTreeNode = NexusMods.Paths.Trees.KeyedBox; diff --git a/src/Abstractions/NexusMods.Abstractions.Installers/ModInstallerResult.cs b/src/Abstractions/NexusMods.Abstractions.Installers/ModInstallerResult.cs index 4729381f5a..f3cb3d4ac0 100644 --- a/src/Abstractions/NexusMods.Abstractions.Installers/ModInstallerResult.cs +++ b/src/Abstractions/NexusMods.Abstractions.Installers/ModInstallerResult.cs @@ -1,6 +1,4 @@ using JetBrains.Annotations; -using NexusMods.Abstractions.DataModel.Entities.Sorting; -using NexusMods.Abstractions.Loadouts.Ids; using NexusMods.Abstractions.Loadouts.Mods; using NexusMods.MnemonicDB.Abstractions.Models; diff --git a/src/Abstractions/NexusMods.Abstractions.Installers/NexusMods.Abstractions.Installers.csproj b/src/Abstractions/NexusMods.Abstractions.Installers/NexusMods.Abstractions.Installers.csproj index d7cee6417e..fa3af61b3d 100644 --- a/src/Abstractions/NexusMods.Abstractions.Installers/NexusMods.Abstractions.Installers.csproj +++ b/src/Abstractions/NexusMods.Abstractions.Installers/NexusMods.Abstractions.Installers.csproj @@ -3,7 +3,6 @@ - @@ -16,21 +15,6 @@ - - ILibraryItemInstaller.cs - - - ILibraryItemInstaller.cs - - - ILibraryItemInstaller.cs - - - ILibraryFileInstaller.cs - - - ILibraryArchiveInstaller.cs - IModInstaller.cs diff --git a/src/Abstractions/NexusMods.Abstractions.Jobs/AJob.cs b/src/Abstractions/NexusMods.Abstractions.Jobs/AJob.cs index 26ddaecb4a..53f9b45691 100644 --- a/src/Abstractions/NexusMods.Abstractions.Jobs/AJob.cs +++ b/src/Abstractions/NexusMods.Abstractions.Jobs/AJob.cs @@ -42,7 +42,8 @@ public abstract class AJob : IJobGroup, IDisposable, IAsyncDisposable protected AJob( MutableProgress progress, IJobGroup? group = default, - IJobWorker? worker = default) + IJobWorker? worker = default, + IJobMonitor? monitor = default) { Id = JobId.NewId(); Status = JobStatus.None; @@ -64,6 +65,8 @@ protected AJob( _collection = []; _observableCollection = new ObservableCollection(_collection); ObservableCollection = new ReadOnlyObservableCollection(_observableCollection); + + monitor?.RegisterJob(this); } public IEnumerator GetEnumerator() => _collection.GetEnumerator(); diff --git a/src/Abstractions/NexusMods.Abstractions.Jobs/IJobMonitor.cs b/src/Abstractions/NexusMods.Abstractions.Jobs/IJobMonitor.cs new file mode 100644 index 0000000000..91592501ee --- /dev/null +++ b/src/Abstractions/NexusMods.Abstractions.Jobs/IJobMonitor.cs @@ -0,0 +1,27 @@ +using System.Collections.ObjectModel; +using DynamicData; +using JetBrains.Annotations; + +namespace NexusMods.Abstractions.Jobs; + +/// +/// Represents a monitor for jobs. +/// +[PublicAPI] +public interface IJobMonitor +{ + /// + /// Gets an observable collection containing every job the monitor knows about. + /// + ReadOnlyObservableCollection Jobs { get; } + + /// + /// Gets an observable with changeset for jobs of type . + /// + IObservable> GetObservableChangeSet() where TJob : IJob; + + /// + /// Registers a job with the monitor. + /// + void RegisterJob(IJob job); +} diff --git a/src/Abstractions/NexusMods.Abstractions.Jobs/JobResult.cs b/src/Abstractions/NexusMods.Abstractions.Jobs/JobResult.cs index 8b082bf9c7..e5f85bdd09 100644 --- a/src/Abstractions/NexusMods.Abstractions.Jobs/JobResult.cs +++ b/src/Abstractions/NexusMods.Abstractions.Jobs/JobResult.cs @@ -78,18 +78,25 @@ public bool TryGetFailed([NotNullWhen(true)] out JobResultFailed? failed) return true; } + /// + /// Returns the data in the completed result or thrown an exception. + /// + /// The result isn't of type public TData RequireData() where TData : notnull { if (!TryGetCompleted(out var completed)) - throw new InvalidOperationException($"JobResult is of type `{ResultType}` but expected `{JobResultType.Completed}`"); + throw new InvalidOperationException($"JobResult is of type `{ResultType}` but expected `{JobResultType.Completed}`: `{ToString()}`"); if (!completed.TryGetData(out var data)) - throw new InvalidOperationException("Completed JobResult doesn't have data!"); + throw new InvalidOperationException($"Completed JobResult doesn't have data: `{ToString()}`"); return data; } + /// + /// Creates a failed job result from an exception. + /// [StackTraceHidden] public static JobResult CreateFailed(Exception exception) { @@ -99,15 +106,23 @@ public static JobResult CreateFailed(Exception exception) }); } + /// + /// Creates a failed job result from an error message. + /// [StackTraceHidden] public static JobResult CreateFailed(string message) { return CreateFailed(new Exception(message)); } + /// + /// Creates a cancelled job result. + /// [StackTraceHidden] public static JobResult CreateCancelled() => new(new JobResultCancelled()); - [StackTraceHidden] public static JobResult CreateCompleted() => new(new JobResultCompleted()); + /// + /// Creates a completed job result with data. + /// [StackTraceHidden] public static JobResult CreateCompleted(TData data) where TData : notnull @@ -117,4 +132,14 @@ public static JobResult CreateCompleted(TData data) Data = data, }); } + + /// + public override string ToString() + { + return _value.Match( + f0: x => x.ToString(), + f1: x => x.ToString(), + f2: x => x.ToString() + ); + } } diff --git a/src/Abstractions/NexusMods.Abstractions.Jobs/JobWorker.cs b/src/Abstractions/NexusMods.Abstractions.Jobs/JobWorker.cs index 1a62d3f384..2099b49caa 100644 --- a/src/Abstractions/NexusMods.Abstractions.Jobs/JobWorker.cs +++ b/src/Abstractions/NexusMods.Abstractions.Jobs/JobWorker.cs @@ -12,7 +12,7 @@ public delegate Task ExecuteAsyncDelegateWithData(TJob job, public delegate Task ExecuteAsyncDelegate(TJob job, AJobWorker worker, CancellationToken cancellationToken) where TJob : AJob; - public static AJobWorker Create(TJob job, ExecuteAsyncDelegateWithData func) + public static AJobWorker CreateWithData(TJob job, ExecuteAsyncDelegateWithData func) where TJob : AJob where TData : notnull { diff --git a/src/Abstractions/NexusMods.Abstractions.Installers/ALibraryArchiveInstaller.cs b/src/Abstractions/NexusMods.Abstractions.Library.Installers/ALibraryArchiveInstaller.cs similarity index 95% rename from src/Abstractions/NexusMods.Abstractions.Installers/ALibraryArchiveInstaller.cs rename to src/Abstractions/NexusMods.Abstractions.Library.Installers/ALibraryArchiveInstaller.cs index b503cb9e2c..e93d9ed50e 100644 --- a/src/Abstractions/NexusMods.Abstractions.Installers/ALibraryArchiveInstaller.cs +++ b/src/Abstractions/NexusMods.Abstractions.Library.Installers/ALibraryArchiveInstaller.cs @@ -1,10 +1,10 @@ using JetBrains.Annotations; using Microsoft.Extensions.Logging; -using NexusMods.Abstractions.Library; +using NexusMods.Abstractions.Library.Models; using NexusMods.Abstractions.Loadouts; using NexusMods.MnemonicDB.Abstractions; -namespace NexusMods.Abstractions.Installers; +namespace NexusMods.Abstractions.Library.Installers; /// /// Base implementation of . diff --git a/src/Abstractions/NexusMods.Abstractions.Installers/ALibraryFileInstaller.cs b/src/Abstractions/NexusMods.Abstractions.Library.Installers/ALibraryFileInstaller.cs similarity index 95% rename from src/Abstractions/NexusMods.Abstractions.Installers/ALibraryFileInstaller.cs rename to src/Abstractions/NexusMods.Abstractions.Library.Installers/ALibraryFileInstaller.cs index 601542c220..1eb1014ec9 100644 --- a/src/Abstractions/NexusMods.Abstractions.Installers/ALibraryFileInstaller.cs +++ b/src/Abstractions/NexusMods.Abstractions.Library.Installers/ALibraryFileInstaller.cs @@ -1,10 +1,10 @@ using JetBrains.Annotations; using Microsoft.Extensions.Logging; -using NexusMods.Abstractions.Library; +using NexusMods.Abstractions.Library.Models; using NexusMods.Abstractions.Loadouts; using NexusMods.MnemonicDB.Abstractions; -namespace NexusMods.Abstractions.Installers; +namespace NexusMods.Abstractions.Library.Installers; /// /// Base implementation of . diff --git a/src/Abstractions/NexusMods.Abstractions.Installers/ALibraryItemInstaller.cs b/src/Abstractions/NexusMods.Abstractions.Library.Installers/ALibraryItemInstaller.cs similarity index 92% rename from src/Abstractions/NexusMods.Abstractions.Installers/ALibraryItemInstaller.cs rename to src/Abstractions/NexusMods.Abstractions.Library.Installers/ALibraryItemInstaller.cs index 58420b4ca2..3bd3e49755 100644 --- a/src/Abstractions/NexusMods.Abstractions.Installers/ALibraryItemInstaller.cs +++ b/src/Abstractions/NexusMods.Abstractions.Library.Installers/ALibraryItemInstaller.cs @@ -1,10 +1,10 @@ using JetBrains.Annotations; using Microsoft.Extensions.Logging; -using NexusMods.Abstractions.Library; +using NexusMods.Abstractions.Library.Models; using NexusMods.Abstractions.Loadouts; using NexusMods.MnemonicDB.Abstractions; -namespace NexusMods.Abstractions.Installers; +namespace NexusMods.Abstractions.Library.Installers; /// /// Base implementation of . diff --git a/src/Abstractions/NexusMods.Abstractions.Installers/ILibraryArchiveInstaller.cs b/src/Abstractions/NexusMods.Abstractions.Library.Installers/ILibraryArchiveInstaller.cs similarity index 89% rename from src/Abstractions/NexusMods.Abstractions.Installers/ILibraryArchiveInstaller.cs rename to src/Abstractions/NexusMods.Abstractions.Library.Installers/ILibraryArchiveInstaller.cs index d5fe51ba66..0d2f4a6fdd 100644 --- a/src/Abstractions/NexusMods.Abstractions.Installers/ILibraryArchiveInstaller.cs +++ b/src/Abstractions/NexusMods.Abstractions.Library.Installers/ILibraryArchiveInstaller.cs @@ -1,9 +1,9 @@ using JetBrains.Annotations; -using NexusMods.Abstractions.Library; +using NexusMods.Abstractions.Library.Models; using NexusMods.Abstractions.Loadouts; using NexusMods.MnemonicDB.Abstractions; -namespace NexusMods.Abstractions.Installers; +namespace NexusMods.Abstractions.Library.Installers; /// /// Variant of for diff --git a/src/Abstractions/NexusMods.Abstractions.Installers/ILibraryFileInstaller.cs b/src/Abstractions/NexusMods.Abstractions.Library.Installers/ILibraryFileInstaller.cs similarity index 89% rename from src/Abstractions/NexusMods.Abstractions.Installers/ILibraryFileInstaller.cs rename to src/Abstractions/NexusMods.Abstractions.Library.Installers/ILibraryFileInstaller.cs index 1128ba3369..14d85f606b 100644 --- a/src/Abstractions/NexusMods.Abstractions.Installers/ILibraryFileInstaller.cs +++ b/src/Abstractions/NexusMods.Abstractions.Library.Installers/ILibraryFileInstaller.cs @@ -1,9 +1,9 @@ using JetBrains.Annotations; -using NexusMods.Abstractions.Library; +using NexusMods.Abstractions.Library.Models; using NexusMods.Abstractions.Loadouts; using NexusMods.MnemonicDB.Abstractions; -namespace NexusMods.Abstractions.Installers; +namespace NexusMods.Abstractions.Library.Installers; /// /// Variant of for . diff --git a/src/Abstractions/NexusMods.Abstractions.Installers/ILibraryItemInstaller.cs b/src/Abstractions/NexusMods.Abstractions.Library.Installers/ILibraryItemInstaller.cs similarity index 88% rename from src/Abstractions/NexusMods.Abstractions.Installers/ILibraryItemInstaller.cs rename to src/Abstractions/NexusMods.Abstractions.Library.Installers/ILibraryItemInstaller.cs index 29d5d73887..d7f2020116 100644 --- a/src/Abstractions/NexusMods.Abstractions.Installers/ILibraryItemInstaller.cs +++ b/src/Abstractions/NexusMods.Abstractions.Library.Installers/ILibraryItemInstaller.cs @@ -1,9 +1,9 @@ using JetBrains.Annotations; -using NexusMods.Abstractions.Library; +using NexusMods.Abstractions.Library.Models; using NexusMods.Abstractions.Loadouts; using NexusMods.MnemonicDB.Abstractions; -namespace NexusMods.Abstractions.Installers; +namespace NexusMods.Abstractions.Library.Installers; /// /// Turns into . diff --git a/src/Abstractions/NexusMods.Abstractions.Library.Installers/NexusMods.Abstractions.Library.Installers.csproj b/src/Abstractions/NexusMods.Abstractions.Library.Installers/NexusMods.Abstractions.Library.Installers.csproj new file mode 100644 index 0000000000..c87c227886 --- /dev/null +++ b/src/Abstractions/NexusMods.Abstractions.Library.Installers/NexusMods.Abstractions.Library.Installers.csproj @@ -0,0 +1,23 @@ + + + + + + + + + + + + + ILibraryArchiveInstaller.cs + + + ILibraryFileInstaller.cs + + + ILibraryItemInstaller.cs + + + + diff --git a/src/Abstractions/NexusMods.Abstractions.Library/LibraryArchive.cs b/src/Abstractions/NexusMods.Abstractions.Library.Models/LibraryArchive.cs similarity index 93% rename from src/Abstractions/NexusMods.Abstractions.Library/LibraryArchive.cs rename to src/Abstractions/NexusMods.Abstractions.Library.Models/LibraryArchive.cs index 0a4957a5b5..18fa778547 100644 --- a/src/Abstractions/NexusMods.Abstractions.Library/LibraryArchive.cs +++ b/src/Abstractions/NexusMods.Abstractions.Library.Models/LibraryArchive.cs @@ -2,7 +2,7 @@ using NexusMods.MnemonicDB.Abstractions.Attributes; using NexusMods.MnemonicDB.Abstractions.Models; -namespace NexusMods.Abstractions.Library; +namespace NexusMods.Abstractions.Library.Models; /// /// Represents an archive in the library. diff --git a/src/Abstractions/NexusMods.Abstractions.Library/LibraryArchiveFileEntry.cs b/src/Abstractions/NexusMods.Abstractions.Library.Models/LibraryArchiveFileEntry.cs similarity index 86% rename from src/Abstractions/NexusMods.Abstractions.Library/LibraryArchiveFileEntry.cs rename to src/Abstractions/NexusMods.Abstractions.Library.Models/LibraryArchiveFileEntry.cs index 18ca85ec87..e094149485 100644 --- a/src/Abstractions/NexusMods.Abstractions.Library/LibraryArchiveFileEntry.cs +++ b/src/Abstractions/NexusMods.Abstractions.Library.Models/LibraryArchiveFileEntry.cs @@ -1,9 +1,8 @@ using JetBrains.Annotations; -using NexusMods.Abstractions.MnemonicDB.Attributes; using NexusMods.MnemonicDB.Abstractions.Attributes; using NexusMods.MnemonicDB.Abstractions.Models; -namespace NexusMods.Abstractions.Library; +namespace NexusMods.Abstractions.Library.Models; /// /// Represents a file inside a library archive. diff --git a/src/Abstractions/NexusMods.Abstractions.Library/LibraryFile.cs b/src/Abstractions/NexusMods.Abstractions.Library.Models/LibraryFile.cs similarity index 94% rename from src/Abstractions/NexusMods.Abstractions.Library/LibraryFile.cs rename to src/Abstractions/NexusMods.Abstractions.Library.Models/LibraryFile.cs index e5821acc8c..ef268c7bd8 100644 --- a/src/Abstractions/NexusMods.Abstractions.Library/LibraryFile.cs +++ b/src/Abstractions/NexusMods.Abstractions.Library.Models/LibraryFile.cs @@ -2,7 +2,7 @@ using NexusMods.Abstractions.MnemonicDB.Attributes; using NexusMods.MnemonicDB.Abstractions.Models; -namespace NexusMods.Abstractions.Library; +namespace NexusMods.Abstractions.Library.Models; /// /// Represents a that is a file in the library. diff --git a/src/Abstractions/NexusMods.Abstractions.Library/LibraryItem.cs b/src/Abstractions/NexusMods.Abstractions.Library.Models/LibraryItem.cs similarity index 90% rename from src/Abstractions/NexusMods.Abstractions.Library/LibraryItem.cs rename to src/Abstractions/NexusMods.Abstractions.Library.Models/LibraryItem.cs index 1ebd1c30b0..2fd3674994 100644 --- a/src/Abstractions/NexusMods.Abstractions.Library/LibraryItem.cs +++ b/src/Abstractions/NexusMods.Abstractions.Library.Models/LibraryItem.cs @@ -2,7 +2,7 @@ using NexusMods.MnemonicDB.Abstractions.Attributes; using NexusMods.MnemonicDB.Abstractions.Models; -namespace NexusMods.Abstractions.Library; +namespace NexusMods.Abstractions.Library.Models; /// /// Represents an item in the library. diff --git a/src/Abstractions/NexusMods.Abstractions.Library/LocalFile.cs b/src/Abstractions/NexusMods.Abstractions.Library.Models/LocalFile.cs similarity index 92% rename from src/Abstractions/NexusMods.Abstractions.Library/LocalFile.cs rename to src/Abstractions/NexusMods.Abstractions.Library.Models/LocalFile.cs index 0b94a808cd..21617cf340 100644 --- a/src/Abstractions/NexusMods.Abstractions.Library/LocalFile.cs +++ b/src/Abstractions/NexusMods.Abstractions.Library.Models/LocalFile.cs @@ -2,7 +2,7 @@ using NexusMods.MnemonicDB.Abstractions.Attributes; using NexusMods.MnemonicDB.Abstractions.Models; -namespace NexusMods.Abstractions.Library; +namespace NexusMods.Abstractions.Library.Models; /// /// Represents a local file in the library. diff --git a/src/Abstractions/NexusMods.Abstractions.Library.Models/NexusMods.Abstractions.Library.Models.csproj b/src/Abstractions/NexusMods.Abstractions.Library.Models/NexusMods.Abstractions.Library.Models.csproj new file mode 100644 index 0000000000..b27970d59b --- /dev/null +++ b/src/Abstractions/NexusMods.Abstractions.Library.Models/NexusMods.Abstractions.Library.Models.csproj @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + LibraryItem.cs + + + LibraryItem.cs + + + LibraryArchive.cs + + + diff --git a/src/Abstractions/NexusMods.Abstractions.Library/Services.cs b/src/Abstractions/NexusMods.Abstractions.Library.Models/Services.cs similarity index 85% rename from src/Abstractions/NexusMods.Abstractions.Library/Services.cs rename to src/Abstractions/NexusMods.Abstractions.Library.Models/Services.cs index 33a0624467..c0e2a7e836 100644 --- a/src/Abstractions/NexusMods.Abstractions.Library/Services.cs +++ b/src/Abstractions/NexusMods.Abstractions.Library.Models/Services.cs @@ -1,8 +1,7 @@ using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; -using NexusMods.Abstractions.MnemonicDB.Attributes; -namespace NexusMods.Abstractions.Library; +namespace NexusMods.Abstractions.Library.Models; /// /// Extension methods. diff --git a/src/Abstractions/NexusMods.Abstractions.Library/ILibraryService.cs b/src/Abstractions/NexusMods.Abstractions.Library/ILibraryService.cs index 67702580f5..1dee0edf14 100644 --- a/src/Abstractions/NexusMods.Abstractions.Library/ILibraryService.cs +++ b/src/Abstractions/NexusMods.Abstractions.Library/ILibraryService.cs @@ -1,6 +1,9 @@ using JetBrains.Annotations; +using NexusMods.Abstractions.Downloads; using NexusMods.Abstractions.Jobs; +using NexusMods.Abstractions.Loadouts; using NexusMods.Paths; +using LibraryItem = NexusMods.Abstractions.Library.Models.LibraryItem; namespace NexusMods.Abstractions.Library; @@ -10,8 +13,20 @@ namespace NexusMods.Abstractions.Library; [PublicAPI] public interface ILibraryService { + /// + /// Adds a download to the library. + /// + IJob AddDownload(IDownloadJob downloadJob); + /// /// Adds a local file to the library. /// IJob AddLocalFile(AbsolutePath absolutePath); + + /// + /// Installs a library item into a target loadout. + /// + /// The item to install. + /// The target loadout. + IJob InstallItem(LibraryItem.ReadOnly libraryItem, Loadout.ReadOnly targetLoadout); } diff --git a/src/Abstractions/NexusMods.Abstractions.Library/NexusMods.Abstractions.Library.csproj b/src/Abstractions/NexusMods.Abstractions.Library/NexusMods.Abstractions.Library.csproj index c838fae56b..5301250070 100644 --- a/src/Abstractions/NexusMods.Abstractions.Library/NexusMods.Abstractions.Library.csproj +++ b/src/Abstractions/NexusMods.Abstractions.Library/NexusMods.Abstractions.Library.csproj @@ -4,22 +4,8 @@ - - - - - + + - - - LibraryItem.cs - - - LibraryItem.cs - - - LibraryArchive.cs - - diff --git a/src/Abstractions/NexusMods.Abstractions.Loadouts/LibraryLinkedLoadoutItem.cs b/src/Abstractions/NexusMods.Abstractions.Loadouts/LibraryLinkedLoadoutItem.cs index fcef28fb93..a964f3a865 100644 --- a/src/Abstractions/NexusMods.Abstractions.Loadouts/LibraryLinkedLoadoutItem.cs +++ b/src/Abstractions/NexusMods.Abstractions.Loadouts/LibraryLinkedLoadoutItem.cs @@ -1,5 +1,5 @@ using JetBrains.Annotations; -using NexusMods.Abstractions.Library; +using NexusMods.Abstractions.Library.Models; using NexusMods.MnemonicDB.Abstractions.Attributes; using NexusMods.MnemonicDB.Abstractions.Models; diff --git a/src/Abstractions/NexusMods.Abstractions.Loadouts/LoadoutItem.cs b/src/Abstractions/NexusMods.Abstractions.Loadouts/LoadoutItem.cs index c0a48596b9..edae3d3433 100644 --- a/src/Abstractions/NexusMods.Abstractions.Loadouts/LoadoutItem.cs +++ b/src/Abstractions/NexusMods.Abstractions.Loadouts/LoadoutItem.cs @@ -1,5 +1,5 @@ using JetBrains.Annotations; -using NexusMods.Abstractions.Library; +using NexusMods.Abstractions.Library.Models; using NexusMods.Abstractions.MnemonicDB.Attributes; using NexusMods.MnemonicDB.Abstractions.Attributes; using NexusMods.MnemonicDB.Abstractions.Models; @@ -37,9 +37,4 @@ public partial class LoadoutItem : IModelDefinition /// Optional parent of the item. /// public static readonly ReferenceAttribute Parent = new(Namespace, nameof(Parent)) { IsIndexed = true, IsOptional = true }; - - /// - /// Optional source of the item. - /// - public static readonly ReferenceAttribute Source = new(Namespace, nameof(Source)) { IsIndexed = true, IsOptional = true }; } diff --git a/src/Abstractions/NexusMods.Abstractions.Loadouts/NexusMods.Abstractions.Loadouts.csproj b/src/Abstractions/NexusMods.Abstractions.Loadouts/NexusMods.Abstractions.Loadouts.csproj index 6fa6ed2177..c519166dcc 100644 --- a/src/Abstractions/NexusMods.Abstractions.Loadouts/NexusMods.Abstractions.Loadouts.csproj +++ b/src/Abstractions/NexusMods.Abstractions.Loadouts/NexusMods.Abstractions.Loadouts.csproj @@ -7,7 +7,7 @@ - + all diff --git a/src/Abstractions/NexusMods.Abstractions.NexusModsLibrary/NexusMods.Abstractions.NexusModsLibrary.csproj b/src/Abstractions/NexusMods.Abstractions.NexusModsLibrary/NexusMods.Abstractions.NexusModsLibrary.csproj index 7fca010760..e7df41743e 100644 --- a/src/Abstractions/NexusMods.Abstractions.NexusModsLibrary/NexusMods.Abstractions.NexusModsLibrary.csproj +++ b/src/Abstractions/NexusMods.Abstractions.NexusModsLibrary/NexusMods.Abstractions.NexusModsLibrary.csproj @@ -3,7 +3,7 @@ - + diff --git a/src/Abstractions/NexusMods.Abstractions.NexusModsLibrary/NexusModsLibraryFile.cs b/src/Abstractions/NexusMods.Abstractions.NexusModsLibrary/NexusModsLibraryFile.cs index 27e321daa1..cd50405b3d 100644 --- a/src/Abstractions/NexusMods.Abstractions.NexusModsLibrary/NexusModsLibraryFile.cs +++ b/src/Abstractions/NexusMods.Abstractions.NexusModsLibrary/NexusModsLibraryFile.cs @@ -1,5 +1,5 @@ using JetBrains.Annotations; -using NexusMods.Abstractions.Library; +using NexusMods.Abstractions.Library.Models; using NexusMods.MnemonicDB.Abstractions.Attributes; using NexusMods.MnemonicDB.Abstractions.Models; diff --git a/src/NexusMods.App/NexusMods.App.csproj b/src/NexusMods.App/NexusMods.App.csproj index 7ea30baad5..f2fd49fab9 100644 --- a/src/NexusMods.App/NexusMods.App.csproj +++ b/src/NexusMods.App/NexusMods.App.csproj @@ -24,6 +24,8 @@ + + diff --git a/src/NexusMods.App/Services.cs b/src/NexusMods.App/Services.cs index 3260c7260e..a32599e5db 100644 --- a/src/NexusMods.App/Services.cs +++ b/src/NexusMods.App/Services.cs @@ -3,6 +3,7 @@ using NexusMods.Abstractions.FileStore; using NexusMods.Abstractions.Games; using NexusMods.Abstractions.Installers; +using NexusMods.Abstractions.Library.Models; using NexusMods.Abstractions.Loadouts; using NexusMods.Abstractions.Serialization; using NexusMods.Abstractions.Settings; @@ -21,6 +22,8 @@ using NexusMods.Games.Generic; using NexusMods.Games.Reshade; using NexusMods.Games.TestHarness; +using NexusMods.Jobs; +using NexusMods.Library; using NexusMods.Networking.Downloaders; using NexusMods.Networking.HttpDownloader; using NexusMods.Networking.NexusWebApi; @@ -56,6 +59,10 @@ public static IServiceCollection AddApp(this IServiceCollection services, { services .AddDataModel() + .AddLibrary() + .AddLibraryModels() + .AddJobMonitor() + .AddSettings() .AddSettings() .AddSettings() diff --git a/src/NexusMods.DataModel/ArchiveInstaller.cs b/src/NexusMods.DataModel/ArchiveInstaller.cs index 5f725109c0..9cd7e3b1c4 100644 --- a/src/NexusMods.DataModel/ArchiveInstaller.cs +++ b/src/NexusMods.DataModel/ArchiveInstaller.cs @@ -2,17 +2,19 @@ using Microsoft.Extensions.Logging; using NexusMods.Abstractions.Activities; using NexusMods.Abstractions.FileStore; -using NexusMods.Abstractions.FileStore.ArchiveMetadata; using NexusMods.Abstractions.FileStore.Downloads; using NexusMods.Abstractions.Games; using NexusMods.Abstractions.Installers; using NexusMods.Abstractions.IO; +using NexusMods.Abstractions.Library; using NexusMods.Abstractions.Loadouts; -using NexusMods.Abstractions.Loadouts.Ids; using NexusMods.Abstractions.Loadouts.Mods; +using NexusMods.App.BuildInfo; using NexusMods.Extensions.BCL; +using NexusMods.Hashing.xxHash64; using NexusMods.MnemonicDB.Abstractions; using File = NexusMods.Abstractions.Loadouts.Files.File; +using LibraryFile = NexusMods.Abstractions.Library.Models.LibraryFile; namespace NexusMods.DataModel; @@ -22,6 +24,7 @@ namespace NexusMods.DataModel; public class ArchiveInstaller : IArchiveInstaller { private readonly ILogger _logger; + private readonly ILibraryService _libraryService; private readonly IConnection _conn; private readonly IActivityFactory _activityFactory; private readonly IFileStore _fileStore; @@ -31,7 +34,9 @@ public class ArchiveInstaller : IArchiveInstaller /// /// DI Constructor /// - public ArchiveInstaller(ILogger logger, + public ArchiveInstaller( + ILogger logger, + ILibraryService libraryService, IFileOriginRegistry fileOriginRegistry, IConnection conn, IFileStore fileStore, @@ -39,13 +44,37 @@ public ArchiveInstaller(ILogger logger, IServiceProvider provider) { _logger = logger; + _libraryService = libraryService; _conn = conn; _fileOriginRegistry = fileOriginRegistry; _fileStore = fileStore; _activityFactory = activityFactory; _provider = provider; } - + + private async Task ShadowTrafficTestLibraryService(Hash hash, Loadout.ReadOnly loadout, CancellationToken cancellationToken) + { + // TODO: https://github.com/Nexus-Mods/NexusMods.App/issues/1763 + if (!CompileConstants.IsDebug) return; + try + { + if (!LibraryFile.FindByHash(_conn.Db, hash).TryGetFirst(out var libraryFile)) + { + _logger.LogDebug("Found no library item with hash `{Hash}`, skipping shadow traffic test", hash); + return; + } + + var job = _libraryService.InstallItem(libraryFile.AsLibraryItem(), loadout); + await job.StartAsync(cancellationToken: cancellationToken); + var result = await job.WaitToFinishAsync(cancellationToken: cancellationToken); + _logger.LogInformation("InstallItem result: `{Result}`", result.ToString()); + } + catch (Exception e) + { + _logger.LogError(e, "Exception install library item"); + } + } + /// public async Task AddMods( LoadoutId loadoutId, @@ -57,7 +86,9 @@ public async Task AddMods( // Get the loadout and create the mod, so we can use it in the job. var useCustomInstaller = installer != null; var loadout = Loadout.Load(_conn.Db, loadoutId); - + + await ShadowTrafficTestLibraryService(download.Hash, loadout, token); + // Note(suggestedName) cannot be null here. // Because string is non-nullable where it is set (FileOriginRegistry), // and using that is a prerequisite to calling this function. diff --git a/src/NexusMods.DataModel/FileOriginRegistry.cs b/src/NexusMods.DataModel/FileOriginRegistry.cs index da92fb3dfc..10d99442a4 100644 --- a/src/NexusMods.DataModel/FileOriginRegistry.cs +++ b/src/NexusMods.DataModel/FileOriginRegistry.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; using NexusMods.Abstractions.DiskState; using NexusMods.Abstractions.FileExtractor; @@ -7,6 +8,8 @@ using NexusMods.Abstractions.FileStore.Downloads; using NexusMods.Abstractions.IO; using NexusMods.Abstractions.IO.StreamFactories; +using NexusMods.Abstractions.Library; +using NexusMods.App.BuildInfo; using NexusMods.Extensions.Hashing; using NexusMods.Hashing.xxHash64; using NexusMods.MnemonicDB.Abstractions; @@ -23,6 +26,7 @@ namespace NexusMods.DataModel; public class FileOriginRegistry : IFileOriginRegistry { private readonly ILogger _logger; + private readonly ILibraryService _libraryService; private readonly IFileExtractor _extractor; private readonly IFileStore _fileStore; private readonly TemporaryFileManager _temporaryFileManager; @@ -30,17 +34,19 @@ public class FileOriginRegistry : IFileOriginRegistry private readonly IFileHashCache _fileHashCache; /// - /// DI Constructor + /// Constructor. /// - /// - /// - /// - /// - /// - public FileOriginRegistry(ILogger logger, IFileExtractor extractor, - IFileStore fileStore, TemporaryFileManager temporaryFileManager, IConnection conn, IFileHashCache fileHashCache) + public FileOriginRegistry( + ILogger logger, + ILibraryService library, + IFileExtractor extractor, + IFileStore fileStore, + TemporaryFileManager temporaryFileManager, + IConnection conn, + IFileHashCache fileHashCache) { _logger = logger; + _libraryService = library; _extractor = extractor; _fileStore = fileStore; _temporaryFileManager = temporaryFileManager; @@ -72,9 +78,28 @@ void AppendNestedArchiveMetadata(ITransaction tx, EntityId id) } } + private async Task ShadowTrafficTestLibraryService(AbsolutePath path, CancellationToken cancellationToken) + { + // TODO: https://github.com/Nexus-Mods/NexusMods.App/issues/1763 + if (!CompileConstants.IsDebug) return; + try + { + var job = _libraryService.AddLocalFile(path); + await job.StartAsync(cancellationToken: cancellationToken); + var result = await job.WaitToFinishAsync(cancellationToken: cancellationToken); + _logger.LogInformation("AddLocalFile result: `{Result}`", result.ToString()); + } + catch (Exception e) + { + _logger.LogError(e, "Exception adding local file to library"); + } + } + /// public async ValueTask RegisterDownload(AbsolutePath path, MetadataFn metaDataFn, string modName, CancellationToken token = default) { + await ShadowTrafficTestLibraryService(path, token); + var db = _conn.Db; var archiveSize = (ulong) path.FileInfo.Size; var archiveHash = (await _fileHashCache.IndexFileAsync(path, token)).Hash; @@ -88,8 +113,11 @@ public async ValueTask RegisterDownload(AbsolutePath path, MetadataF return await RegisterFolderInternal(tmpFolder.Path, metaDataFn, null, _fileStore.GetFileHashes(), archiveHash.Value, archiveSize, modName, token); } + /// public async ValueTask RegisterDownload(AbsolutePath path, EntityId id, string modName, CancellationToken token = default) { + await ShadowTrafficTestLibraryService(path, token); + var db = _conn.Db; var archiveSize = (ulong) path.FileInfo.Size; diff --git a/src/NexusMods.DataModel/NexusMods.DataModel.csproj b/src/NexusMods.DataModel/NexusMods.DataModel.csproj index da8ec57fb4..f6bd200d22 100644 --- a/src/NexusMods.DataModel/NexusMods.DataModel.csproj +++ b/src/NexusMods.DataModel/NexusMods.DataModel.csproj @@ -4,6 +4,7 @@ + diff --git a/src/NexusMods.Jobs/JobMonitor.cs b/src/NexusMods.Jobs/JobMonitor.cs new file mode 100644 index 0000000000..21a624f12d --- /dev/null +++ b/src/NexusMods.Jobs/JobMonitor.cs @@ -0,0 +1,43 @@ +using System.Collections.ObjectModel; +using System.Reactive.Disposables; +using DynamicData; +using JetBrains.Annotations; +using NexusMods.Abstractions.Jobs; + +namespace NexusMods.Jobs; + +[UsedImplicitly] +public sealed class JobMonitor : IJobMonitor, IDisposable +{ + private readonly SourceCache _jobSourceCache = new(job => job.Id); + private readonly ReadOnlyObservableCollection _jobs; + public ReadOnlyObservableCollection Jobs => _jobs; + + private readonly CompositeDisposable _compositeDisposable = new(); + + public JobMonitor() + { + var disposable = _jobSourceCache + .Connect() + .Bind(out _jobs) + .Subscribe(); + + _compositeDisposable.Add(disposable); + } + + public IObservable> GetObservableChangeSet() where TJob : IJob + { + return _jobSourceCache.Connect().OfType(); + } + + public void RegisterJob(IJob job) + { + _jobSourceCache.AddOrUpdate(job); + } + + public void Dispose() + { + _compositeDisposable.Dispose(); + _jobSourceCache.Dispose(); + } +} diff --git a/src/NexusMods.Jobs/NexusMods.Jobs.csproj b/src/NexusMods.Jobs/NexusMods.Jobs.csproj new file mode 100644 index 0000000000..d341e0f871 --- /dev/null +++ b/src/NexusMods.Jobs/NexusMods.Jobs.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/src/NexusMods.Jobs/Services.cs b/src/NexusMods.Jobs/Services.cs new file mode 100644 index 0000000000..e6c1d012bf --- /dev/null +++ b/src/NexusMods.Jobs/Services.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.DependencyInjection; +using NexusMods.Abstractions.Jobs; + +namespace NexusMods.Jobs; + +public static class Services +{ + public static IServiceCollection AddJobMonitor(this IServiceCollection serviceCollection) + { + return serviceCollection.AddSingleton(); + } +} diff --git a/src/NexusMods.Library/AddLibraryFileJob.cs b/src/NexusMods.Library/AddLibraryFileJob.cs index 658245ca14..a1ba63400e 100644 --- a/src/NexusMods.Library/AddLibraryFileJob.cs +++ b/src/NexusMods.Library/AddLibraryFileJob.cs @@ -1,6 +1,6 @@ using DynamicData.Kernel; using NexusMods.Abstractions.Jobs; -using NexusMods.Abstractions.Library; +using NexusMods.Abstractions.Library.Models; using NexusMods.MnemonicDB.Abstractions; using NexusMods.Paths; @@ -14,15 +14,17 @@ public AddLibraryFileJob(IJobGroup? group = null, IJobWorker? worker = null) public required ITransaction Transaction { get; init; } public required AbsolutePath FilePath { get; init; } public required bool DoCommit { get; init; } + public required bool DoBackup { get; init; } public Optional EntityId { get; set; } public Optional IsArchive { get; set; } + public Optional HasBackup { get; set; } public Optional HashJobResult { get; set; } public Optional LibraryFile { get; set; } public Optional LibraryArchive { get; set; } public Optional ExtractionDirectory { get; set; } public Optional ExtractedFiles { get; set; } - public Optional AddExtractedFileJobResults { get; set; } + public Optional[]> AddExtractedFileJobResults { get; set; } protected override void Dispose(bool disposing) { diff --git a/src/NexusMods.Library/AddLibraryFileJobWorker.cs b/src/NexusMods.Library/AddLibraryFileJobWorker.cs index a7c98227d3..d82fff64c3 100644 --- a/src/NexusMods.Library/AddLibraryFileJobWorker.cs +++ b/src/NexusMods.Library/AddLibraryFileJobWorker.cs @@ -2,9 +2,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NexusMods.Abstractions.FileExtractor; +using NexusMods.Abstractions.IO; using NexusMods.Abstractions.IO.StreamFactories; using NexusMods.Abstractions.Jobs; -using NexusMods.Abstractions.Library; +using NexusMods.Abstractions.Library.Models; using NexusMods.Hashing.xxHash64; using NexusMods.MnemonicDB.Abstractions; using NexusMods.Paths; @@ -19,6 +20,7 @@ internal class AddLibraryFileJobWorker : AJobWorker private readonly IServiceProvider _serviceProvider; private readonly IFileExtractor _fileExtractor; private readonly TemporaryFileManager _temporaryFileManager; + private readonly IFileStore _fileStore; public AddLibraryFileJobWorker(IServiceProvider serviceProvider) { @@ -27,6 +29,7 @@ public AddLibraryFileJobWorker(IServiceProvider serviceProvider) _serviceProvider = serviceProvider; _fileExtractor = serviceProvider.GetRequiredService(); _temporaryFileManager = serviceProvider.GetRequiredService(); + _fileStore = serviceProvider.GetRequiredService(); } protected override async Task ExecuteAsync(AddLibraryFileJob job, CancellationToken cancellationToken) @@ -103,28 +106,56 @@ protected override async Task ExecuteAsync(AddLibraryFileJob job, Can if (!job.AddExtractedFileJobResults.HasValue) { var extractedFiles = job.ExtractedFiles.Value; - var results = new JobResult[extractedFiles.Length]; + var results = new ValueTuple[extractedFiles.Length]; await Parallel.ForAsync(fromInclusive: 0, toExclusive: extractedFiles.Length, cancellationToken, async (i, innerCancellationToken) => { + var fileEntry = extractedFiles[i]; + var worker = _serviceProvider.GetRequiredService(); var childJob = new AddLibraryFileJob(job, worker) { Transaction = job.Transaction, - FilePath = extractedFiles[i].Path, + FilePath = fileEntry.Path, DoCommit = false, + DoBackup = false, }; await worker.StartAsync(childJob, cancellationToken: innerCancellationToken); var result = await childJob.WaitToFinishAsync(cancellationToken: innerCancellationToken); - results[i] = result; + results[i] = (result, fileEntry); }); + + job.AddExtractedFileJobResults = results; + } + + cancellationToken.ThrowIfCancellationRequested(); + if (!job.HasBackup.HasValue) + { + var filesToBackup = job.AddExtractedFileJobResults.Value + .Select(tuple => + { + var (jobResult, fileEntry) = tuple; + var data = jobResult.RequireData(); + + return new ArchivedFileEntry + { + Hash = data.Hash, + Size = data.Size, + StreamFactory = new NativeFileStreamFactory(fileEntry.Path), + }; + }) + .ToArray(); + + await _fileStore.BackupFiles(filesToBackup, token: cancellationToken); + job.HasBackup = true; } cancellationToken.ThrowIfCancellationRequested(); - foreach (var jobResult in job.AddExtractedFileJobResults.Value) + foreach (var tuple in job.AddExtractedFileJobResults.Value) { + var (jobResult, _) = tuple; var libraryFile = jobResult.RequireData(); var archiveFileEntry = new LibraryArchiveFileEntry.New(job.Transaction, libraryFile.Id) { @@ -133,6 +164,22 @@ await Parallel.ForAsync(fromInclusive: 0, toExclusive: extractedFiles.Length, ca }; } } + else + { + cancellationToken.ThrowIfCancellationRequested(); + if (job is { DoBackup: true, HasBackup.HasValue: false }) + { + var archivedFileEntry = new ArchivedFileEntry + { + Hash = job.HashJobResult.Value.RequireData(), + Size = job.FilePath.FileInfo.Size, + StreamFactory = new NativeFileStreamFactory(job.FilePath), + }; + + await _fileStore.BackupFiles([archivedFileEntry], token: cancellationToken); + job.HasBackup = true; + } + } cancellationToken.ThrowIfCancellationRequested(); if (job.DoCommit) diff --git a/src/NexusMods.Library/AddLocalFileJobWorker.cs b/src/NexusMods.Library/AddLocalFileJobWorker.cs index e5304cabd0..39a5b2dd38 100644 --- a/src/NexusMods.Library/AddLocalFileJobWorker.cs +++ b/src/NexusMods.Library/AddLocalFileJobWorker.cs @@ -1,7 +1,7 @@ using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; using NexusMods.Abstractions.Jobs; -using NexusMods.Abstractions.Library; +using NexusMods.Abstractions.Library.Models; namespace NexusMods.Library; @@ -25,6 +25,7 @@ protected override async Task ExecuteAsync(AddLocalFileJob job, Cance Transaction = job.Transaction, FilePath = job.FilePath, DoCommit = false, + DoBackup = true, }; await worker.StartAsync(addLibraryFileJob, cancellationToken: cancellationToken); diff --git a/src/NexusMods.Library/InstallLoadoutItemJob.cs b/src/NexusMods.Library/InstallLoadoutItemJob.cs new file mode 100644 index 0000000000..d1de69bd56 --- /dev/null +++ b/src/NexusMods.Library/InstallLoadoutItemJob.cs @@ -0,0 +1,32 @@ +using DynamicData.Kernel; +using NexusMods.Abstractions.Jobs; +using NexusMods.Abstractions.Library.Installers; +using NexusMods.Abstractions.Library.Models; +using NexusMods.Abstractions.Loadouts; +using NexusMods.MnemonicDB.Abstractions; + +namespace NexusMods.Library; + +public class InstallLoadoutItemJob : AJob +{ + public InstallLoadoutItemJob( + IJobGroup? group = default, + IJobWorker? worker = default, + IJobMonitor? monitor = default) : base(new MutableProgress(new IndeterminateProgress()), group, worker, monitor) { } + + public required ITransaction Transaction { get; init; } + public required LibraryItem.ReadOnly LibraryItem { get; init; } + public required Loadout.ReadOnly Loadout { get; init; } + + public Optional Installer { get; set; } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + Transaction.Dispose(); + } + + base.Dispose(disposing); + } +} diff --git a/src/NexusMods.Library/InstallLoadoutItemJobWorker.cs b/src/NexusMods.Library/InstallLoadoutItemJobWorker.cs new file mode 100644 index 0000000000..1d5f48635c --- /dev/null +++ b/src/NexusMods.Library/InstallLoadoutItemJobWorker.cs @@ -0,0 +1,54 @@ +using DynamicData.Kernel; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NexusMods.Abstractions.Games; +using NexusMods.Abstractions.Installers; +using NexusMods.Abstractions.Jobs; +using NexusMods.Abstractions.Library.Installers; +using NexusMods.Abstractions.Loadouts; + +namespace NexusMods.Library; + +[UsedImplicitly] +internal class InstallLoadoutItemJobWorker : AJobWorker +{ + private readonly ILogger _logger; + + public InstallLoadoutItemJobWorker(IServiceProvider serviceProvider) + { + _logger = serviceProvider.GetRequiredService>(); + } + + protected override async Task ExecuteAsync(InstallLoadoutItemJob job, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (!job.Installer.HasValue) + { + ILibraryItemInstaller? foundInstaller = null; + var installers = job.Loadout.InstallationInstance.GetGame().LibraryItemInstallers; + foreach (var installer in installers) + { + var isSupported = await installer.IsSupportedAsync(job.LibraryItem, cancellationToken); + if (!isSupported) continue; + + foundInstaller = installer; + break; + } + + // TODO: default to advanced installer + if (foundInstaller is null) + { + return JobResult.CreateFailed($"Found no installer that supports `{job.LibraryItem.Name}` (`{job.LibraryItem.Id}`)"); + } + + job.Installer = Optional.Create(foundInstaller); + } + + var result = await job.Installer.Value.ExecuteAsync(job.LibraryItem, job.Transaction, job.Loadout, cancellationToken); + var transactionResult = await job.Transaction.Commit(); + + var jobResults = result.Select(x => transactionResult.Remap(x)).ToArray(); + return JobResult.CreateCompleted(jobResults); + } +} diff --git a/src/NexusMods.Library/LibraryService.cs b/src/NexusMods.Library/LibraryService.cs index 4734ecb011..a136d393d5 100644 --- a/src/NexusMods.Library/LibraryService.cs +++ b/src/NexusMods.Library/LibraryService.cs @@ -1,7 +1,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using NexusMods.Abstractions.Downloads; using NexusMods.Abstractions.Jobs; using NexusMods.Abstractions.Library; +using NexusMods.Abstractions.Library.Models; +using NexusMods.Abstractions.Loadouts; using NexusMods.MnemonicDB.Abstractions; using NexusMods.Paths; @@ -26,6 +29,11 @@ public LibraryService(ILogger logger, IConnection connection, IS _serviceProvider = serviceProvider; } + public IJob AddDownload(IDownloadJob downloadJob) + { + throw new NotImplementedException(); + } + public IJob AddLocalFile(AbsolutePath absolutePath) { var group = new AddLocalFileJob(worker: _serviceProvider.GetRequiredService()) @@ -36,4 +44,16 @@ public IJob AddLocalFile(AbsolutePath absolutePath) return group; } + + public IJob InstallItem(LibraryItem.ReadOnly libraryItem, Loadout.ReadOnly targetLoadout) + { + var job = new InstallLoadoutItemJob(worker: _serviceProvider.GetRequiredService()) + { + Transaction = _connection.BeginTransaction(), + LibraryItem = libraryItem, + Loadout = targetLoadout, + }; + + return job; + } } diff --git a/src/NexusMods.Library/NexusMods.Library.csproj b/src/NexusMods.Library/NexusMods.Library.csproj index f141f3c01f..942fb20815 100644 --- a/src/NexusMods.Library/NexusMods.Library.csproj +++ b/src/NexusMods.Library/NexusMods.Library.csproj @@ -2,7 +2,10 @@ + + + diff --git a/src/NexusMods.Library/Services.cs b/src/NexusMods.Library/Services.cs index a1c343062b..b1188e3a44 100644 --- a/src/NexusMods.Library/Services.cs +++ b/src/NexusMods.Library/Services.cs @@ -15,6 +15,11 @@ public static class Services /// public static IServiceCollection AddLibrary(this IServiceCollection serviceCollection) { - return serviceCollection.AddSingleton(); + return serviceCollection + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); } } diff --git a/src/NexusMods.StandardGameLocators/Unknown/UnknownGame.cs b/src/NexusMods.StandardGameLocators/Unknown/UnknownGame.cs index b07f941a0e..b77d96f656 100644 --- a/src/NexusMods.StandardGameLocators/Unknown/UnknownGame.cs +++ b/src/NexusMods.StandardGameLocators/Unknown/UnknownGame.cs @@ -4,6 +4,7 @@ using NexusMods.Abstractions.Games.DTO; using NexusMods.Abstractions.Installers; using NexusMods.Abstractions.IO; +using NexusMods.Abstractions.Library.Installers; using NexusMods.Abstractions.Loadouts.Mods; using NexusMods.Abstractions.Loadouts.Synchronizers; using NexusMods.Abstractions.Serialization; @@ -60,6 +61,9 @@ public void ResetInstallations() { } /// public ILoadoutSynchronizer Synchronizer => throw new NotImplementedException(); + /// + public ILibraryItemInstaller[] LibraryItemInstallers { get; } = []; + /// public GameInstallation InstallationFromLocatorResult(GameLocatorResult metadata, EntityId dbId, IGameLocator locator) { diff --git a/tests/Games/NexusMods.Games.TestFramework/DependencyInjectionHelper.cs b/tests/Games/NexusMods.Games.TestFramework/DependencyInjectionHelper.cs index a63a7064c3..a696adfbd5 100644 --- a/tests/Games/NexusMods.Games.TestFramework/DependencyInjectionHelper.cs +++ b/tests/Games/NexusMods.Games.TestFramework/DependencyInjectionHelper.cs @@ -2,14 +2,16 @@ using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using NexusMods.Abstractions.Activities; using NexusMods.Abstractions.HttpDownloader; +using NexusMods.Abstractions.Library.Models; using NexusMods.Abstractions.Loadouts.Synchronizers; using NexusMods.Abstractions.Settings; using NexusMods.Activities; using NexusMods.CrossPlatform; using NexusMods.DataModel; using NexusMods.FileExtractor; +using NexusMods.Jobs; +using NexusMods.Library; using NexusMods.Networking.HttpDownloader; using NexusMods.Networking.NexusWebApi; using NexusMods.Paths; @@ -59,6 +61,9 @@ public static IServiceCollection AddDefaultServicesForTesting(this IServiceColle .AddSettings() .AddHttpDownloader() .AddDataModel() + .AddLibrary() + .AddLibraryModels() + .AddJobMonitor() .AddLoadoutsSynchronizers() .OverrideSettingsForTests(settings => settings with { diff --git a/tests/Games/NexusMods.Games.TestFramework/NexusMods.Games.TestFramework.csproj b/tests/Games/NexusMods.Games.TestFramework/NexusMods.Games.TestFramework.csproj index c3b104b69a..83688aa1a7 100644 --- a/tests/Games/NexusMods.Games.TestFramework/NexusMods.Games.TestFramework.csproj +++ b/tests/Games/NexusMods.Games.TestFramework/NexusMods.Games.TestFramework.csproj @@ -5,6 +5,8 @@ + + diff --git a/tests/NexusMods.CLI.Tests/Startup.cs b/tests/NexusMods.CLI.Tests/Startup.cs index 297729ccd0..759f224da8 100644 --- a/tests/NexusMods.CLI.Tests/Startup.cs +++ b/tests/NexusMods.CLI.Tests/Startup.cs @@ -3,6 +3,7 @@ using NexusMods.Abstractions.FileStore; using NexusMods.Abstractions.Games; using NexusMods.Abstractions.Installers; +using NexusMods.Abstractions.Library.Models; using NexusMods.Abstractions.Loadouts; using NexusMods.Abstractions.Serialization; using NexusMods.Abstractions.Settings; @@ -11,6 +12,8 @@ using NexusMods.CrossPlatform; using NexusMods.DataModel; using NexusMods.FileExtractor; +using NexusMods.Jobs; +using NexusMods.Library; using NexusMods.Networking.HttpDownloader; using NexusMods.Networking.HttpDownloader.Tests; using NexusMods.Networking.NexusWebApi; @@ -33,6 +36,9 @@ public void ConfigureServices(IServiceCollection services) .AddFileSystem() .AddSettingsManager() .AddDataModel() + .AddLibrary() + .AddLibraryModels() + .AddJobMonitor() .OverrideSettingsForTests(settings => settings with { UseInMemoryDataModel = true, diff --git a/tests/NexusMods.DataModel.Tests/FileOriginRegistryTests.cs b/tests/NexusMods.DataModel.Tests/FileOriginRegistryTests.cs index f5945fefd9..65cc13e551 100644 --- a/tests/NexusMods.DataModel.Tests/FileOriginRegistryTests.cs +++ b/tests/NexusMods.DataModel.Tests/FileOriginRegistryTests.cs @@ -6,6 +6,7 @@ using NexusMods.Abstractions.FileExtractor; using NexusMods.Abstractions.FileStore; using NexusMods.Abstractions.IO; +using NexusMods.Abstractions.Library; using NexusMods.DataModel.Tests.Harness; using NexusMods.Hashing.xxHash64; using NSubstitute; @@ -21,6 +22,7 @@ public async Task RegisterFolder_ShouldRegisterCorrectly() // Arrange IFileOriginRegistry sut = new FileOriginRegistry( ServiceProvider.GetRequiredService>(), + ServiceProvider.GetRequiredService(), ServiceProvider.GetRequiredService(), FileStore, TemporaryFileManager, @@ -52,6 +54,7 @@ public async Task RegisterFolder_WhenCalledTwice_ShouldBeDedupedOnSameArchive() fileStore.GetFileHashes().Returns(new HashSet()); // not needed here IFileOriginRegistry sut = new FileOriginRegistry( ServiceProvider.GetRequiredService>(), + ServiceProvider.GetRequiredService(), ServiceProvider.GetRequiredService(), fileStore, TemporaryFileManager, @@ -78,6 +81,7 @@ public async Task RegisterFolder_WhenCalledTwice_ShouldOnlyAppendNewData() IFileOriginRegistry sut = new FileOriginRegistry( ServiceProvider.GetRequiredService>(), + ServiceProvider.GetRequiredService(), ServiceProvider.GetRequiredService(), fileStore, TemporaryFileManager, diff --git a/tests/NexusMods.DataModel.Tests/NexusMods.DataModel.Tests.csproj b/tests/NexusMods.DataModel.Tests/NexusMods.DataModel.Tests.csproj index 31c6b97ac7..70be2a13cd 100644 --- a/tests/NexusMods.DataModel.Tests/NexusMods.DataModel.Tests.csproj +++ b/tests/NexusMods.DataModel.Tests/NexusMods.DataModel.Tests.csproj @@ -10,6 +10,8 @@ + + diff --git a/tests/NexusMods.DataModel.Tests/Startup.cs b/tests/NexusMods.DataModel.Tests/Startup.cs index 83b750799f..60c984cf42 100644 --- a/tests/NexusMods.DataModel.Tests/Startup.cs +++ b/tests/NexusMods.DataModel.Tests/Startup.cs @@ -4,6 +4,7 @@ using NexusMods.Abstractions.Games; using NexusMods.Abstractions.GuidedInstallers; using NexusMods.Abstractions.Installers; +using NexusMods.Abstractions.Library.Models; using NexusMods.Abstractions.Loadouts; using NexusMods.Abstractions.Serialization; using NexusMods.Abstractions.Serialization.ExpressionGenerator; @@ -12,6 +13,8 @@ using NexusMods.App.BuildInfo; using NexusMods.CrossPlatform; using NexusMods.FileExtractor; +using NexusMods.Jobs; +using NexusMods.Library; using NexusMods.Paths; using NexusMods.Settings; using NexusMods.StandardGameLocators; @@ -49,6 +52,9 @@ public static IServiceCollection AddServices(IServiceCollection container) .AddSingleton(new TemporaryFileManager(FileSystem.Shared, prefix)) .AddSettingsManager() .AddDataModel() + .AddLibrary() + .AddLibraryModels() + .AddJobMonitor() .OverrideSettingsForTests(settings => settings with { UseInMemoryDataModel = true, diff --git a/tests/NexusMods.Jobs.Tests/JobWorkerTests.cs b/tests/NexusMods.Jobs.Tests/JobWorkerTests.cs index f6106d798d..4bd8cf60ff 100644 --- a/tests/NexusMods.Jobs.Tests/JobWorkerTests.cs +++ b/tests/NexusMods.Jobs.Tests/JobWorkerTests.cs @@ -10,7 +10,7 @@ public class JobWorkerTests public async Task TestCreateSync() { var job = new MyJob(); - var worker = JobWorker.Create(new MyJob(), (_, _, _) => Task.FromResult("hello world")); + var worker = JobWorker.CreateWithData(new MyJob(), (_, _, _) => Task.FromResult("hello world")); await worker.StartAsync(job); var jobResult = await job.WaitToFinishAsync(); @@ -23,7 +23,7 @@ public async Task TestCreateSync() public async Task TestCreateAsync() { var job = new MyJob(); - var worker = JobWorker.Create(new MyJob(), async (_, _, _) => + var worker = JobWorker.CreateWithData(new MyJob(), async (_, _, _) => { await Task.Yield(); return "hello world";