diff --git a/Wabbajack.App.Wpf/Settings.cs b/Wabbajack.App.Wpf/Settings.cs index 4ad1517c3..c380bfae5 100644 --- a/Wabbajack.App.Wpf/Settings.cs +++ b/Wabbajack.App.Wpf/Settings.cs @@ -1,4 +1,5 @@ -using Wabbajack.Downloaders; +using SteamKit2.GC.Dota.Internal; +using Wabbajack.Downloaders; using Wabbajack.DTOs.JsonConverters; using Wabbajack.Paths; using Wabbajack.RateLimiter; @@ -14,41 +15,65 @@ public class Mo2ModlistInstallationSettings public bool AutomaticallyOverrideExistingInstall { get; set; } } - public class PerformanceSettings : ViewModel + public class PerformanceSettingsViewModel : ViewModel { private readonly Configuration.MainSettings _settings; - private readonly int _defaultMaximumMemoryPerDownloadThreadMb; + private readonly int _defaultMaximumMemoryPerDownloadThreadMB; + private readonly long _defaultMinimumFileSizeForResumableDownloadMB; - public PerformanceSettings(Configuration.MainSettings settings, IResource downloadResources, SystemParametersConstructor systemParams) + public PerformanceSettingsViewModel(Configuration.MainSettings settings, IResource downloadResources, SystemParametersConstructor systemParams) { var p = systemParams.Create(); _settings = settings; // Split half of available memory among download threads - _defaultMaximumMemoryPerDownloadThreadMb = (int)(p.SystemMemorySize / downloadResources.MaxTasks / 1024 / 1024) / 2; - _maximumMemoryPerDownloadThreadMb = settings.PerformanceSettings.MaximumMemoryPerDownloadThreadMb; + _defaultMaximumMemoryPerDownloadThreadMB = (int)(p.SystemMemorySize / downloadResources.MaxTasks / 1024 / 1024) / 2; + _defaultMinimumFileSizeForResumableDownloadMB = long.MaxValue; + _maximumMemoryPerDownloadThreadMB = settings.MaximumMemoryPerDownloadThreadInMB; + _minimumFileSizeForResumableDownloadMB = settings.MinimumFileSizeForResumableDownloadMB; if (MaximumMemoryPerDownloadThreadMb < 0) { ResetMaximumMemoryPerDownloadThreadMb(); } + + if (settings.MinimumFileSizeForResumableDownloadMB < 0) + { + ResetMinimumFileSizeForResumableDownload(); + } } - private int _maximumMemoryPerDownloadThreadMb; + private int _maximumMemoryPerDownloadThreadMB; + private long _minimumFileSizeForResumableDownloadMB; public int MaximumMemoryPerDownloadThreadMb { - get => _maximumMemoryPerDownloadThreadMb; + get => _maximumMemoryPerDownloadThreadMB; + set + { + RaiseAndSetIfChanged(ref _maximumMemoryPerDownloadThreadMB, value); + _settings.MaximumMemoryPerDownloadThreadInMB = value; + } + } + + public long MinimumFileSizeForResumableDownload + { + get => _minimumFileSizeForResumableDownloadMB; set { - RaiseAndSetIfChanged(ref _maximumMemoryPerDownloadThreadMb, value); - _settings.PerformanceSettings.MaximumMemoryPerDownloadThreadMb = value; + RaiseAndSetIfChanged(ref _minimumFileSizeForResumableDownloadMB, value); + _settings.MinimumFileSizeForResumableDownloadMB = value; } } public void ResetMaximumMemoryPerDownloadThreadMb() { - MaximumMemoryPerDownloadThreadMb = _defaultMaximumMemoryPerDownloadThreadMb; + MaximumMemoryPerDownloadThreadMb = _defaultMaximumMemoryPerDownloadThreadMB; + } + + public void ResetMinimumFileSizeForResumableDownload() + { + MinimumFileSizeForResumableDownload = _defaultMinimumFileSizeForResumableDownloadMB; } } } diff --git a/Wabbajack.App.Wpf/View Models/Compilers/CompilerVM.cs b/Wabbajack.App.Wpf/View Models/Compilers/CompilerVM.cs index f514aea9f..4d586e0b7 100644 --- a/Wabbajack.App.Wpf/View Models/Compilers/CompilerVM.cs +++ b/Wabbajack.App.Wpf/View Models/Compilers/CompilerVM.cs @@ -46,7 +46,7 @@ public class CompilerVM : BackNavigatingVM, ICpuStatusVM { private const string LastSavedCompilerSettings = "last-saved-compiler-settings"; private readonly DTOSerializer _dtos; - private readonly SettingsManager _settingsManager; + private readonly ISettingsManager _settingsManager; private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; private readonly ResourceMonitor _resourceMonitor; @@ -106,7 +106,7 @@ public class CompilerVM : BackNavigatingVM, ICpuStatusVM [Reactive] public ErrorResponse ErrorState { get; private set; } - public CompilerVM(ILogger logger, DTOSerializer dtos, SettingsManager settingsManager, + public CompilerVM(ILogger logger, DTOSerializer dtos, ISettingsManager settingsManager, IServiceProvider serviceProvider, LogStream loggerProvider, ResourceMonitor resourceMonitor, CompilerSettingsInferencer inferencer, Client wjClient, IEnumerable logins, DownloadDispatcher downloadDispatcher) : base(logger) { diff --git a/Wabbajack.App.Wpf/View Models/Gallery/ModListGalleryVM.cs b/Wabbajack.App.Wpf/View Models/Gallery/ModListGalleryVM.cs index 48045dcf9..202278f6d 100644 --- a/Wabbajack.App.Wpf/View Models/Gallery/ModListGalleryVM.cs +++ b/Wabbajack.App.Wpf/View Models/Gallery/ModListGalleryVM.cs @@ -1,6 +1,4 @@ - - -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; @@ -77,13 +75,13 @@ public GameTypeEntry SelectedGameTypeEntry private readonly ILogger _logger; private readonly GameLocator _locator; private readonly ModListDownloadMaintainer _maintainer; - private readonly SettingsManager _settingsManager; + private readonly ISettingsManager _settingsManager; private readonly CancellationToken _cancellationToken; public ICommand ClearFiltersCommand { get; set; } public ModListGalleryVM(ILogger logger, Client wjClient, GameLocator locator, - SettingsManager settingsManager, ModListDownloadMaintainer maintainer, CancellationToken cancellationToken) + ISettingsManager settingsManager, ModListDownloadMaintainer maintainer, CancellationToken cancellationToken) : base(logger) { _wjClient = wjClient; diff --git a/Wabbajack.App.Wpf/View Models/Installers/InstallerVM.cs b/Wabbajack.App.Wpf/View Models/Installers/InstallerVM.cs index 99918e1bc..41ff8b755 100644 --- a/Wabbajack.App.Wpf/View Models/Installers/InstallerVM.cs +++ b/Wabbajack.App.Wpf/View Models/Installers/InstallerVM.cs @@ -15,7 +15,6 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Shell; -using System.Windows.Threading; using Microsoft.Extensions.Logging; using Microsoft.WindowsAPICodePack.Dialogs; using Wabbajack.Common; @@ -34,7 +33,6 @@ using Wabbajack.Paths.IO; using Wabbajack.Services.OSIntegrated; using Wabbajack.Util; -using System.Windows.Forms; using Microsoft.Extensions.DependencyInjection; using Wabbajack.CLI.Verbs; using Wabbajack.VFS; @@ -114,7 +112,7 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM private readonly DTOSerializer _dtos; private readonly ILogger _logger; - private readonly SettingsManager _settingsManager; + private readonly ISettingsManager _settingsManager; private readonly IServiceProvider _serviceProvider; private readonly SystemParametersConstructor _parametersConstructor; private readonly IGameLocator _gameLocator; @@ -156,7 +154,7 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM public ReactiveCommand VerifyCommand { get; } - public InstallerVM(ILogger logger, DTOSerializer dtos, SettingsManager settingsManager, IServiceProvider serviceProvider, + public InstallerVM(ILogger logger, DTOSerializer dtos, ISettingsManager settingsManager, IServiceProvider serviceProvider, SystemParametersConstructor parametersConstructor, IGameLocator gameLocator, LogStream loggerProvider, ResourceMonitor resourceMonitor, Wabbajack.Services.OSIntegrated.Configuration configuration, HttpClient client, DownloadDispatcher dispatcher, IEnumerable logins, CancellationToken cancellationToken) : base(logger) diff --git a/Wabbajack.App.Wpf/View Models/Settings/SettingsVM.cs b/Wabbajack.App.Wpf/View Models/Settings/SettingsVM.cs index a32855cec..06f5641ab 100644 --- a/Wabbajack.App.Wpf/View Models/Settings/SettingsVM.cs +++ b/Wabbajack.App.Wpf/View Models/Settings/SettingsVM.cs @@ -24,10 +24,10 @@ namespace Wabbajack public class SettingsVM : BackNavigatingVM { private readonly Configuration.MainSettings _settings; - private readonly SettingsManager _settingsManager; + private readonly ISettingsManager _settingsManager; public LoginManagerVM Login { get; } - public PerformanceSettings Performance { get; } + public PerformanceSettingsViewModel Performance { get; } public AuthorFilesVM AuthorFile { get; } public ICommand OpenTerminalCommand { get; } @@ -36,14 +36,14 @@ public SettingsVM(ILogger logger, IServiceProvider provider) : base(logger) { _settings = provider.GetRequiredService(); - _settingsManager = provider.GetRequiredService(); + _settingsManager = provider.GetRequiredService(); Login = new LoginManagerVM(provider.GetRequiredService>(), this, provider.GetRequiredService>()); AuthorFile = new AuthorFilesVM(provider.GetRequiredService>()!, provider.GetRequiredService()!, provider.GetRequiredService()!, this); OpenTerminalCommand = ReactiveCommand.CreateFromTask(OpenTerminal); - Performance = new PerformanceSettings( + Performance = new PerformanceSettingsViewModel( _settings, provider.GetRequiredService>(), provider.GetRequiredService()); diff --git a/Wabbajack.App.Wpf/Views/Settings/PerformanceSettingsView.xaml b/Wabbajack.App.Wpf/Views/Settings/PerformanceSettingsView.xaml index d976f2b94..ddc9b950b 100644 --- a/Wabbajack.App.Wpf/Views/Settings/PerformanceSettingsView.xaml +++ b/Wabbajack.App.Wpf/Views/Settings/PerformanceSettingsView.xaml @@ -9,7 +9,7 @@ xmlns:xwpf="http://schemas.xceed.com/wpf/xaml/toolkit" d:DesignHeight="450" d:DesignWidth="800" - x:TypeArguments="local:PerformanceSettings" + x:TypeArguments="local:PerformanceSettingsViewModel" mc:Ignorable="d"> Reset + + + diff --git a/Wabbajack.App.Wpf/Views/Settings/PerformanceSettingsView.xaml.cs b/Wabbajack.App.Wpf/Views/Settings/PerformanceSettingsView.xaml.cs index d2a0ee5c4..e6e1d1169 100644 --- a/Wabbajack.App.Wpf/Views/Settings/PerformanceSettingsView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Settings/PerformanceSettingsView.xaml.cs @@ -8,7 +8,7 @@ namespace Wabbajack /// /// Interaction logic for PerformanceSettingsView.xaml /// - public partial class PerformanceSettingsView : ReactiveUserControl + public partial class PerformanceSettingsView : ReactiveUserControl { public PerformanceSettingsView() { @@ -21,16 +21,29 @@ public PerformanceSettingsView() x => x.MaximumMemoryPerDownloadThreadMb, x => x.MaximumMemoryPerDownloadThreadIntegerUpDown.Value) .DisposeWith(disposable); + + this.BindStrict( + ViewModel, + x => x.MinimumFileSizeForResumableDownload, + x => x.MinimumFileSizeForResumableDownloadIntegerUpDown.Value) + .DisposeWith(disposable); + this.EditResourceSettings.Command = ReactiveCommand.Create(() => { UIUtils.OpenFile( KnownFolders.WabbajackAppLocal.Combine("saved_settings", "resource_settings.json")); Environment.Exit(0); }); + ResetMaximumMemoryPerDownloadThread.Command = ReactiveCommand.Create(() => { ViewModel.ResetMaximumMemoryPerDownloadThreadMb(); }); + + ResetMinimumFileSizeForResumableDownload.Command = ReactiveCommand.Create(() => + { + ViewModel.ResetMinimumFileSizeForResumableDownload(); + }); }); } } diff --git a/Wabbajack.CLI/Program.cs b/Wabbajack.CLI/Program.cs index 94208892b..bb13ed675 100644 --- a/Wabbajack.CLI/Program.cs +++ b/Wabbajack.CLI/Program.cs @@ -1,7 +1,6 @@ using System; using System.CommandLine; using System.CommandLine.IO; -using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -11,14 +10,13 @@ using NLog.Targets; using Octokit; using Wabbajack.DTOs.Interventions; -using Wabbajack.Networking.Http; -using Wabbajack.Networking.Http.Interfaces; using Wabbajack.Paths.IO; using Wabbajack.Server.Lib; using Wabbajack.Services.OSIntegrated; using Wabbajack.VFS; using Client = Wabbajack.Networking.GitHub.Client; using Wabbajack.CLI.Builder; +using Wabbajack.Downloader.Services; namespace Wabbajack.CLI; @@ -31,8 +29,7 @@ private static async Task Main(string[] args) .ConfigureServices((host, services) => { services.AddSingleton(new JsonSerializerOptions()); - services.AddSingleton(); - services.AddSingleton(); + services.AddDownloaderService(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/Wabbajack.CLI/VerbRegistration.cs b/Wabbajack.CLI/VerbRegistration.cs index 69634dde2..93ed9e134 100644 --- a/Wabbajack.CLI/VerbRegistration.cs +++ b/Wabbajack.CLI/VerbRegistration.cs @@ -45,8 +45,6 @@ public static void AddCLIVerbs(this IServiceCollection services) { services.AddSingleton(); CommandLineBuilder.RegisterCommand(ModlistReport.Definition, c => ((ModlistReport)c).Run); services.AddSingleton(); -CommandLineBuilder.RegisterCommand(SetNexusApiKey.Definition, c => ((SetNexusApiKey)c).Run); -services.AddSingleton(); CommandLineBuilder.RegisterCommand(SteamDownloadFile.Definition, c => ((SteamDownloadFile)c).Run); services.AddSingleton(); CommandLineBuilder.RegisterCommand(SteamDumpAppInfo.Definition, c => ((SteamDumpAppInfo)c).Run); diff --git a/Wabbajack.CLI/Verbs/SetNexusApiKey.cs b/Wabbajack.CLI/Verbs/SetNexusApiKey.cs deleted file mode 100644 index a66576441..000000000 --- a/Wabbajack.CLI/Verbs/SetNexusApiKey.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Wabbajack.CLI.Builder; -using Wabbajack.DTOs.Logins; -using Wabbajack.Services.OSIntegrated; - -namespace Wabbajack.CLI.Verbs; - -public class SetNexusApiKey -{ - private readonly EncryptedJsonTokenProvider _tokenProvider; - private readonly ILogger _logger; - - public SetNexusApiKey(EncryptedJsonTokenProvider tokenProvider, ILogger logger) - { - _tokenProvider = tokenProvider; - _logger = logger; - } - - public static VerbDefinition Definition = new("set-nexus-api-key", - "Sets the Nexus API key to the specified value", - [ - new OptionDefinition(typeof(string), "k", "key", "The Nexus API key") - ]); - - public async Task Run(string key) - { - if (string.IsNullOrEmpty(key)) - { - _logger.LogInformation("Not setting Nexus API key, that looks like an empty string to me."); - return -1; - } - else - { - await _tokenProvider.SetToken(new() { ApiKey = key }); - _logger.LogInformation("Set Nexus API Key to {key}", key); - return 0; - } - } -} \ No newline at end of file diff --git a/Wabbajack.Configuration/MainSettings.cs b/Wabbajack.Configuration/MainSettings.cs index 5b8bf4eb8..c9b866183 100644 --- a/Wabbajack.Configuration/MainSettings.cs +++ b/Wabbajack.Configuration/MainSettings.cs @@ -1,13 +1,31 @@ -namespace Wabbajack.Configuration; +using System.Text.Json.Serialization; + +namespace Wabbajack.Configuration; public class MainSettings { public const string SettingsFileName = "app_settings"; - private const int SettingsVersion = 1; + [JsonPropertyName("CurrentSettingsVersion")] public int CurrentSettingsVersion { get; set; } - public PerformanceSettings PerformanceSettings { get; set; } = new(); + public int MaximumMemoryPerDownloadThreadInMB + { + get => Performance.MaximumMemoryPerDownloadThreadMB; + set => Performance.MaximumMemoryPerDownloadThreadMB = value; + } + + public long MinimumFileSizeForResumableDownloadMB { + get => Performance.MinimumFileSizeForResumableDownloadMB; + set => Performance.MinimumFileSizeForResumableDownloadMB = value; + } + + private const int SettingsVersion = 1; + + [JsonInclude] + [JsonPropertyName("PerformanceSettings")] + private PerformanceSettings Performance { get; set; } = new(); + public bool Upgrade() { @@ -18,10 +36,16 @@ public bool Upgrade() if (CurrentSettingsVersion < 1) { - PerformanceSettings.MaximumMemoryPerDownloadThreadMb = -1; + Performance.MaximumMemoryPerDownloadThreadMB = -1; } CurrentSettingsVersion = SettingsVersion; return true; } + + internal class PerformanceSettings + { + public int MaximumMemoryPerDownloadThreadMB { get; set; } = -1; + public long MinimumFileSizeForResumableDownloadMB { get; set; } = -1; + } } \ No newline at end of file diff --git a/Wabbajack.Configuration/PerformanceSettings.cs b/Wabbajack.Configuration/PerformanceSettings.cs deleted file mode 100644 index 93dff2406..000000000 --- a/Wabbajack.Configuration/PerformanceSettings.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Wabbajack.Configuration; - -public class PerformanceSettings -{ - public int MaximumMemoryPerDownloadThreadMb { get; set; } -} \ No newline at end of file diff --git a/Wabbajack.Downloader.Clients/DownloadClientFactory.cs b/Wabbajack.Downloader.Clients/DownloadClientFactory.cs new file mode 100644 index 000000000..f58180ecc --- /dev/null +++ b/Wabbajack.Downloader.Clients/DownloadClientFactory.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.Logging; +using Wabbajack.Configuration; +using Wabbajack.Downloaders.Interfaces; +using Wabbajack.Paths; +using Wabbajack.RateLimiter; + +namespace Wabbajack.Downloader.Services; + +public interface IDownloadClientFactory +{ + public IDownloadClient GetDownloader(HttpRequestMessage msg, AbsolutePath outputPath, IJob job); +} + +public class DownloadClientFactory(MainSettings _settings, ILoggerFactory _loggerFactory, IHttpClientFactory _httpClientFactory) : IDownloadClientFactory +{ + private readonly ILogger _nonResuableDownloaderLogger = _loggerFactory.CreateLogger(); + private readonly ILogger _resumableDownloaderLogger = _loggerFactory.CreateLogger(); + + private NonResumableDownloadClient? _nonReusableDownloader = default; + + public IDownloadClient GetDownloader(HttpRequestMessage msg, AbsolutePath outputPath, IJob job) + { + if (job.Size >= _settings.MinimumFileSizeForResumableDownloadMB * 1024 * 1024) + { + return new ResumableDownloadClient(msg, outputPath, job, _settings.MaximumMemoryPerDownloadThreadInMB, _resumableDownloaderLogger); + } + else + { + _nonReusableDownloader ??= new NonResumableDownloadClient(msg, outputPath, _nonResuableDownloaderLogger, _httpClientFactory); + + return new NonResumableDownloadClient(msg, outputPath, _nonResuableDownloaderLogger, _httpClientFactory); + } + } +} diff --git a/Wabbajack.Downloader.Clients/DownloaderService.cs b/Wabbajack.Downloader.Clients/DownloaderService.cs new file mode 100644 index 000000000..15de71491 --- /dev/null +++ b/Wabbajack.Downloader.Clients/DownloaderService.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.Logging; +using Wabbajack.Hashing.xxHash64; +using Wabbajack.Networking.Http.Interfaces; +using Wabbajack.Paths; +using Wabbajack.RateLimiter; + +namespace Wabbajack.Downloader.Services; + +public class DownloaderService(ILogger _logger, IDownloadClientFactory _httpDownloaderFactory) : IHttpDownloader +{ + public async Task Download(HttpRequestMessage message, AbsolutePath outputPath, IJob job, CancellationToken token) + { + Exception downloadError = null!; + + var downloader = _httpDownloaderFactory.GetDownloader(message, outputPath, job); + + for (var i = 0; i < 3; i++) + { + try + { + return await downloader.Download(token); + } + catch (Exception ex) + { + downloadError = ex; + _logger.LogDebug("Download for '{name}' failed. Retrying...", outputPath.FileName.ToString()); + } + } + + _logger.LogError(downloadError, "Failed to download '{name}' after 3 tries.", outputPath.FileName.ToString()); + + return new Hash(); + } +} \ No newline at end of file diff --git a/Wabbajack.Downloader.Clients/NonResumableDownloadClient.cs b/Wabbajack.Downloader.Clients/NonResumableDownloadClient.cs new file mode 100644 index 000000000..bf2da0a68 --- /dev/null +++ b/Wabbajack.Downloader.Clients/NonResumableDownloadClient.cs @@ -0,0 +1,72 @@ +using Microsoft.Extensions.Logging; +using System.Net.Http.Headers; +using System.Net.Sockets; +using Wabbajack.Downloaders.Interfaces; +using Wabbajack.Hashing.xxHash64; +using Wabbajack.Paths; +using Wabbajack.Paths.IO; + +namespace Wabbajack.Downloader.Services; + +internal class NonResumableDownloadClient(HttpRequestMessage _msg, AbsolutePath _outputPath, ILogger _logger, IHttpClientFactory _httpClientFactory) : IDownloadClient +{ + public async Task Download(CancellationToken token) + { + if (_msg.RequestUri == null) + { + throw new ArgumentException("Request URI is null"); + } + + try + { + return await DownloadStreamDirectlyToFile(_msg.RequestUri, token, _outputPath, 5); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to download '{name}'", _outputPath.FileName.ToString()); + + throw; + } + } + + private async Task DownloadStreamDirectlyToFile(Uri rquestURI, CancellationToken token, AbsolutePath filePath, int retry = 5) + { + try + { + var httpClient = _httpClientFactory.CreateClient("SmallFilesClient"); + using Stream fileStream = GetFileStream(filePath); + var startingPosition = fileStream.Length; + + _logger.LogDebug("Download for '{name}' is starting from {position}...", _outputPath.FileName.ToString(), startingPosition); + httpClient.DefaultRequestHeaders.Range = new RangeHeaderValue(startingPosition, null); //GetStreamAsync does not accept a HttpRequestMessage so we have to set headers on the client itself + + var response = await httpClient.GetStreamAsync(rquestURI, token); + await response.CopyToAsync(fileStream, token); + + return await fileStream.Hash(token); + } + catch (Exception ex) when (ex is SocketException || ex is IOException) + { + _logger.LogWarning(ex, "Failed to download '{name}' due to network error. Retrying...", _outputPath.FileName.ToString()); + + if(retry == 0) + { + throw; + } + + return await DownloadStreamDirectlyToFile(rquestURI, token, _outputPath, retry--); + } + } + + private Stream GetFileStream(AbsolutePath filePath) + { + if (filePath.FileExists()) + { + return filePath.Open(FileMode.Append, FileAccess.Write, FileShare.None); + } + else + { + return filePath.Open(FileMode.Create, FileAccess.Write, FileShare.None); + } + } +} diff --git a/Wabbajack.Networking.Http/ResumableDownloader.cs b/Wabbajack.Downloader.Clients/ResumableDownloadClient.cs similarity index 78% rename from Wabbajack.Networking.Http/ResumableDownloader.cs rename to Wabbajack.Downloader.Clients/ResumableDownloadClient.cs index 078de77d4..dc5a75b46 100644 --- a/Wabbajack.Networking.Http/ResumableDownloader.cs +++ b/Wabbajack.Downloader.Clients/ResumableDownloadClient.cs @@ -1,10 +1,5 @@ -using System; -using System.ComponentModel; -using System.IO; -using System.Net.Http; +using System.ComponentModel; using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; using Downloader; using Microsoft.Extensions.Logging; using Wabbajack.Configuration; @@ -12,30 +7,16 @@ using Wabbajack.Paths; using Wabbajack.Paths.IO; using Wabbajack.RateLimiter; +using Wabbajack.Networking.Http; +using Wabbajack.Downloaders.Interfaces; -namespace Wabbajack.Networking.Http; +namespace Wabbajack.Downloader.Services; -internal class ResumableDownloader +internal class ResumableDownloadClient(HttpRequestMessage _msg, AbsolutePath _outputPath, IJob _job, int _maxMemoryPerDownloadThread, ILogger _logger) : IDownloadClient { - private readonly IJob _job; - private readonly HttpRequestMessage _msg; - private readonly AbsolutePath _outputPath; - private readonly AbsolutePath _packagePath; - private readonly PerformanceSettings _performanceSettings; - private readonly ILogger _logger; private CancellationToken _token; private Exception? _error; - - - public ResumableDownloader(HttpRequestMessage msg, AbsolutePath outputPath, IJob job, PerformanceSettings performanceSettings, ILogger logger) - { - _job = job; - _msg = msg; - _outputPath = outputPath; - _packagePath = outputPath.WithExtension(Extension.FromPath(".download_package")); - _performanceSettings = performanceSettings; - _logger = logger; - } + private AbsolutePath _packagePath = _outputPath.WithExtension(Extension.FromPath(".download_package")); public async Task Download(CancellationToken token) { @@ -80,15 +61,15 @@ public async Task Download(CancellationToken token) } else { - _logger.LogError(_error,"Download for '{name}' encountered error. Throwing...", _outputPath.FileName.ToString()); + _logger.LogError(_error, "Download for '{name}' encountered error. Throwing...", _outputPath.FileName.ToString()); } throw _error; - } - if (downloader.Status == DownloadStatus.Completed) - { - DeletePackage(); + if (downloader.Status == DownloadStatus.Completed) + { + DeletePackage(); + } } if (!_outputPath.FileExists()) @@ -102,17 +83,17 @@ public async Task Download(CancellationToken token) private DownloadConfiguration CreateConfiguration(HttpRequestMessage message) { - var maximumMemoryPerDownloadThreadMb = Math.Max(0, _performanceSettings.MaximumMemoryPerDownloadThreadMb); + var maximumMemoryPerDownloadThreadMb = Math.Max(0, _maxMemoryPerDownloadThread); var configuration = new DownloadConfiguration { - MaximumMemoryBufferBytes = maximumMemoryPerDownloadThreadMb * 1024 * 1024, + MaximumMemoryBufferBytes = maximumMemoryPerDownloadThreadMb, Timeout = (int)TimeSpan.FromSeconds(120).TotalMilliseconds, ReserveStorageSpaceBeforeStartingDownload = true, RequestConfiguration = new RequestConfiguration { Headers = message.Headers.ToWebHeaderCollection(), ProtocolVersion = message.Version, - UserAgent = message.Headers.UserAgent.ToString() + UserAgent = message.Headers.UserAgent.ToString() } }; diff --git a/Wabbajack.Downloader.Clients/ServiceExtensions.cs b/Wabbajack.Downloader.Clients/ServiceExtensions.cs new file mode 100644 index 000000000..d00e1fef6 --- /dev/null +++ b/Wabbajack.Downloader.Clients/ServiceExtensions.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; +using Wabbajack.Networking.Http.Interfaces; + +namespace Wabbajack.Downloader.Services; + +public static class ServiceExtensions +{ + public static void AddDownloaderService(this IServiceCollection services) + { + services.AddHttpClient("SmallFilesClient").ConfigureHttpClient(c => c.Timeout = TimeSpan.FromMinutes(5)); + services.AddSingleton(); + services.AddSingleton(); + } +} \ No newline at end of file diff --git a/Wabbajack.Downloader.Clients/Wabbajack.Downloader.Services.csproj b/Wabbajack.Downloader.Clients/Wabbajack.Downloader.Services.csproj new file mode 100644 index 000000000..56b94732b --- /dev/null +++ b/Wabbajack.Downloader.Clients/Wabbajack.Downloader.Services.csproj @@ -0,0 +1,25 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + + + + + + + diff --git a/Wabbajack.Downloaders.Interfaces/IDownloadClient.cs b/Wabbajack.Downloaders.Interfaces/IDownloadClient.cs new file mode 100644 index 000000000..759105bb8 --- /dev/null +++ b/Wabbajack.Downloaders.Interfaces/IDownloadClient.cs @@ -0,0 +1,10 @@ +using System.Threading; +using System.Threading.Tasks; +using Wabbajack.Hashing.xxHash64; + +namespace Wabbajack.Downloaders.Interfaces; + +public interface IDownloadClient +{ + public Task Download(CancellationToken token); +} diff --git a/Wabbajack.Installer/AInstaller.cs b/Wabbajack.Installer/AInstaller.cs index c15fb76f9..bad41e058 100644 --- a/Wabbajack.Installer/AInstaller.cs +++ b/Wabbajack.Installer/AInstaller.cs @@ -370,11 +370,16 @@ public async Task DownloadMissingArchives(List missing, CancellationTok UpdateProgress(1); } } - - await missing - .Shuffle() + + var missingBatches = missing .Where(a => a.State is not Manual) - .PDoAll(async archive => + .Batch(100) + .ToList(); + + List batchTasks = []; + foreach (var batch in missingBatches) + { + batchTasks.Add(batch.PDoAll(async archive => { _logger.LogInformation("Downloading {Archive}", archive.Name); var outputPath = _configuration.Downloads.Combine(archive.Name); @@ -392,7 +397,12 @@ await missing var hash = await DownloadArchive(archive, download, token, outputPath); UpdateProgress(1); - }); + })); + + await Task.Delay(TimeSpan.FromSeconds(10)); // Hitting a Nexus API limit when spinning these downloads up too fast. Need to slow this down. + } + + await Task.WhenAll(batchTasks); } private async Task SendDownloadMetrics(List missing) diff --git a/Wabbajack.Launcher/Program.cs b/Wabbajack.Launcher/Program.cs index 1f5e898b2..8fe905a96 100644 --- a/Wabbajack.Launcher/Program.cs +++ b/Wabbajack.Launcher/Program.cs @@ -1,24 +1,17 @@ using System; using System.Net.Http; -using System.Runtime.InteropServices; -using System.Threading.Tasks; using Avalonia; using Avalonia.ReactiveUI; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Wabbajack.Common; -using Wabbajack.Configuration; using Wabbajack.Downloaders.Http; using Wabbajack.DTOs; using Wabbajack.DTOs.JsonConverters; using Wabbajack.DTOs.Logins; using Wabbajack.Launcher.ViewModels; -using Wabbajack.Networking.Http; using Wabbajack.Networking.Http.Interfaces; using Wabbajack.Networking.NexusApi; -using Wabbajack.Paths; -using Wabbajack.Paths.IO; using Wabbajack.RateLimiter; using Wabbajack.Services.OSIntegrated; using Wabbajack.Services.OSIntegrated.TokenProviders; @@ -40,40 +33,18 @@ public static void Main(string[] args) services.AddNexusApi(); services.AddDTOConverters(); services.AddDTOSerializer(); - - services.AddSingleton(s => new Services.OSIntegrated.Configuration - { - EncryptedDataLocation = KnownFolders.WabbajackAppLocal.Combine("encrypted"), - ModListsDownloadLocation = KnownFolders.EntryPoint.Combine("downloaded_mod_lists"), - SavedSettingsLocation = KnownFolders.WabbajackAppLocal.Combine("saved_settings"), - LogLocation = KnownFolders.LauncherAwarePath.Combine("logs"), - ImageCacheLocation = KnownFolders.WabbajackAppLocal.Combine("image_cache") - }); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(s => GetAppSettings(s, MainSettings.SettingsFileName)); - + services.AddSettings(); + services.AddKnownFoldersConfiguration(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton, NexusApiTokenProvider>(); services.AddSingleton(); services.AddAllSingleton>(s => new Resource("Web Requests", 4)); - services.AddAllSingleton(); - - var version = - $"{ThisAssembly.Git.SemVer.Major}.{ThisAssembly.Git.SemVer.Major}.{ThisAssembly.Git.SemVer.Patch}{ThisAssembly.Git.SemVer.DashLabel}"; - services.AddSingleton(s => new ApplicationInfo - { - ApplicationSlug = "Wabbajack", - ApplicationName = Environment.ProcessPath?.ToAbsolutePath().FileName.ToString() ?? "Wabbajack", - ApplicationSha = ThisAssembly.Git.Sha, - Platform = RuntimeInformation.ProcessArchitecture.ToString(), - OperatingSystemDescription = RuntimeInformation.OSDescription, - RuntimeIdentifier = RuntimeInformation.RuntimeIdentifier, - OSVersion = Environment.OSVersion.VersionString, - Version = version - }); + services.AddHttpDownloader(); + + var version = services.AddApplicationInfo(); + }).Build(); Services = host.Services; @@ -81,18 +52,6 @@ public static void Main(string[] args) .StartWithClassicDesktopLifetime(args); } - private static MainSettings GetAppSettings(IServiceProvider provider, string name) - { - var settingsManager = provider.GetRequiredService(); - var settings = Task.Run(() => settingsManager.Load(name)).Result; - if (settings.Upgrade()) - { - settingsManager.Save(MainSettings.SettingsFileName, settings).FireAndForget(); - } - - return settings; - } - public static IServiceProvider Services { get; set; } // Avalonia configuration, don't remove; also used by visual designer. diff --git a/Wabbajack.Networking.Http/ServiceExtensions.cs b/Wabbajack.Networking.Http/ServiceExtensions.cs deleted file mode 100644 index 42a9796e7..000000000 --- a/Wabbajack.Networking.Http/ServiceExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Wabbajack.Networking.Http.Interfaces; - -namespace Wabbajack.Networking.Http; - -public static class ServiceExtensions -{ - public static void AddHttpDownloader(this IServiceCollection services) - { - services.AddSingleton(); - } -} \ No newline at end of file diff --git a/Wabbajack.Networking.Http/SingleThreadedDownloader.cs b/Wabbajack.Networking.Http/SingleThreadedDownloader.cs deleted file mode 100644 index ed88e3bba..000000000 --- a/Wabbajack.Networking.Http/SingleThreadedDownloader.cs +++ /dev/null @@ -1,162 +0,0 @@ -using System; -using System.Buffers; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Wabbajack.Configuration; -using Wabbajack.Hashing.xxHash64; -using Wabbajack.Networking.Http.Interfaces; -using Wabbajack.Paths; -using Wabbajack.Paths.IO; -using Wabbajack.RateLimiter; - -namespace Wabbajack.Networking.Http; - -public class SingleThreadedDownloader : IHttpDownloader -{ - private readonly HttpClient _client; - private readonly ILogger _logger; - private readonly PerformanceSettings _settings; - - public SingleThreadedDownloader(ILogger logger, HttpClient client, MainSettings settings) - { - _logger = logger; - _client = client; - _settings = settings.PerformanceSettings; - } - - public async Task Download(HttpRequestMessage message, AbsolutePath outputPath, IJob job, - CancellationToken token) - { - Exception downloadError = null!; - var downloader = new ResumableDownloader(message, outputPath, job, _settings, _logger); - for (var i = 0; i < 3; i++) - { - try - { - return await downloader.Download(token); - } - catch (Exception ex) - { - downloadError = ex; - _logger.LogDebug("Download for '{name}' failed. Retrying...", outputPath.FileName.ToString()); - } - } - - _logger.LogError(downloadError, "Failed to download '{name}' after 3 tries.", outputPath.FileName.ToString()); - return new Hash(); - - // using var response = await _client.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, token); - // if (!response.IsSuccessStatusCode) - // throw new HttpException(response); - // - // if (job.Size == 0) - // job.Size = response.Content.Headers.ContentLength ?? 0; - // - // /* Need to make this mulitthreaded to be much use - // if ((response.Content.Headers.ContentLength ?? 0) != 0 && - // response.Headers.AcceptRanges.FirstOrDefault() == "bytes") - // { - // return await ResettingDownloader(response, message, outputPath, job, token); - // } - // */ - // - // await using var stream = await response.Content.ReadAsStreamAsync(token); - // await using var outputStream = outputPath.Open(FileMode.Create, FileAccess.Write); - // return await stream.HashingCopy(outputStream, token, job); - } - - private const int CHUNK_SIZE = 1024 * 1024 * 8; - - private async Task ResettingDownloader(HttpResponseMessage response, HttpRequestMessage message, AbsolutePath outputPath, IJob job, CancellationToken token) - { - - using var rented = MemoryPool.Shared.Rent(CHUNK_SIZE); - var buffer = rented.Memory; - - var hasher = new xxHashAlgorithm(0); - - var running = true; - ulong finalHash = 0; - - var inputStream = await response.Content.ReadAsStreamAsync(token); - await using var outputStream = outputPath.Open(FileMode.Create, FileAccess.Write, FileShare.None); - long writePosition = 0; - - while (running && !token.IsCancellationRequested) - { - var totalRead = 0; - - while (totalRead != buffer.Length) - { - var read = await inputStream.ReadAsync(buffer.Slice(totalRead, buffer.Length - totalRead), - token); - - - if (read == 0) - { - running = false; - break; - } - - if (job != null) - await job.Report(read, token); - - totalRead += read; - } - - var pendingWrite = outputStream.WriteAsync(buffer[..totalRead], token); - if (running) - { - hasher.TransformByteGroupsInternal(buffer.Span); - await pendingWrite; - } - else - { - var preSize = (totalRead >> 5) << 5; - if (preSize > 0) - { - hasher.TransformByteGroupsInternal(buffer[..preSize].Span); - finalHash = hasher.FinalizeHashValueInternal(buffer[preSize..totalRead].Span); - await pendingWrite; - break; - } - - finalHash = hasher.FinalizeHashValueInternal(buffer[..totalRead].Span); - await pendingWrite; - break; - } - - { - writePosition += totalRead; - if (job != null) - await job.Report(totalRead, token); - message = CloneMessage(message); - message.Headers.Range = new RangeHeaderValue(writePosition, writePosition + CHUNK_SIZE); - await inputStream.DisposeAsync(); - response.Dispose(); - response = await _client.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, token); - HttpException.ThrowOnFailure(response); - inputStream = await response.Content.ReadAsStreamAsync(token); - } - } - - await outputStream.FlushAsync(token); - - return new Hash(finalHash); - } - - private HttpRequestMessage CloneMessage(HttpRequestMessage message) - { - var newMsg = new HttpRequestMessage(message.Method, message.RequestUri); - foreach (var header in message.Headers) - { - newMsg.Headers.Add(header.Key, header.Value); - } - return newMsg; - } -} \ No newline at end of file diff --git a/Wabbajack.Networking.Http/Wabbajack.Networking.Http.csproj b/Wabbajack.Networking.Http/Wabbajack.Networking.Http.csproj index 595ab3d1b..4f158d080 100644 --- a/Wabbajack.Networking.Http/Wabbajack.Networking.Http.csproj +++ b/Wabbajack.Networking.Http/Wabbajack.Networking.Http.csproj @@ -9,6 +9,7 @@ + diff --git a/Wabbajack.Services.OSIntegrated/Configuration.cs b/Wabbajack.Services.OSIntegrated/Configuration.cs index 7adf650bf..9aa8dd595 100644 --- a/Wabbajack.Services.OSIntegrated/Configuration.cs +++ b/Wabbajack.Services.OSIntegrated/Configuration.cs @@ -1,4 +1,5 @@ using Wabbajack.Paths; +using Wabbajack.Paths.IO; namespace Wabbajack.Services.OSIntegrated; @@ -8,6 +9,5 @@ public class Configuration public AbsolutePath SavedSettingsLocation { get; set; } public AbsolutePath EncryptedDataLocation { get; set; } public AbsolutePath LogLocation { get; set; } - public AbsolutePath ImageCacheLocation { get; set; } } \ No newline at end of file diff --git a/Wabbajack.Services.OSIntegrated/ResourceSettingsManager.cs b/Wabbajack.Services.OSIntegrated/ResourceSettingsManager.cs index 55c669f13..80165f21e 100644 --- a/Wabbajack.Services.OSIntegrated/ResourceSettingsManager.cs +++ b/Wabbajack.Services.OSIntegrated/ResourceSettingsManager.cs @@ -7,10 +7,10 @@ namespace Wabbajack.Services.OSIntegrated; public class ResourceSettingsManager { - private readonly SettingsManager _manager; + private readonly ISettingsManager _manager; private Dictionary? _settings; - public ResourceSettingsManager(SettingsManager manager) + public ResourceSettingsManager(ISettingsManager manager) { _manager = manager; } diff --git a/Wabbajack.Services.OSIntegrated/ServiceExtensions.cs b/Wabbajack.Services.OSIntegrated/ServiceExtensions.cs index a2c3c5181..dd599af60 100644 --- a/Wabbajack.Services.OSIntegrated/ServiceExtensions.cs +++ b/Wabbajack.Services.OSIntegrated/ServiceExtensions.cs @@ -11,6 +11,7 @@ using Wabbajack.Common; using Wabbajack.Compiler; using Wabbajack.Configuration; +using Wabbajack.Downloader.Services; using Wabbajack.Downloaders; using Wabbajack.Downloaders.GameFile; using Wabbajack.Downloaders.ModDB; @@ -23,7 +24,6 @@ using Wabbajack.Installer; using Wabbajack.Networking.BethesdaNet; using Wabbajack.Networking.Discord; -using Wabbajack.Networking.Http; using Wabbajack.Networking.Http.Interfaces; using Wabbajack.Networking.NexusApi; using Wabbajack.Networking.Steam; @@ -112,18 +112,8 @@ public static IServiceCollection AddOSIntegrated(this IServiceCollection service // Settings - service.AddSingleton(s => new Configuration - { - EncryptedDataLocation = KnownFolders.WabbajackAppLocal.Combine("encrypted"), - ModListsDownloadLocation = KnownFolders.EntryPoint.Combine("downloaded_mod_lists"), - SavedSettingsLocation = KnownFolders.WabbajackAppLocal.Combine("saved_settings"), - LogLocation = KnownFolders.LauncherAwarePath.Combine("logs"), - ImageCacheLocation = KnownFolders.WabbajackAppLocal.Combine("image_cache") - }); - - service.AddSingleton(); - service.AddSingleton(); - service.AddSingleton(s => GetAppSettings(s, MainSettings.SettingsFileName)); + service.AddKnownFoldersConfiguration(); + service.AddSettings(); // Resources @@ -157,7 +147,9 @@ public static IServiceCollection AddOSIntegrated(this IServiceCollection service // Networking service.AddSingleton(); - service.AddAllSingleton(); + + // Downloader + service.AddDownloaderService(); service.AddSteam(); @@ -208,10 +200,16 @@ public static IServiceCollection AddOSIntegrated(this IServiceCollection service service.AddScoped(); service.AddSingleton(); - // Application Info + var version = service.AddApplicationInfo(); + + return service; + } + + public static string AddApplicationInfo(this IServiceCollection services) + { var version = $"{ThisAssembly.Git.SemVer.Major}.{ThisAssembly.Git.SemVer.Major}.{ThisAssembly.Git.SemVer.Patch}{ThisAssembly.Git.SemVer.DashLabel}"; - service.AddSingleton(s => new ApplicationInfo + services.AddSingleton(s => new ApplicationInfo { ApplicationSlug = "Wabbajack", ApplicationName = Environment.ProcessPath?.ToAbsolutePath().FileName.ToString() ?? "Wabbajack", @@ -223,21 +221,32 @@ public static IServiceCollection AddOSIntegrated(this IServiceCollection service Version = version }); - - - return service; + return version; } - - public static MainSettings GetAppSettings(IServiceProvider provider, string name) + + public static IServiceCollection AddKnownFoldersConfiguration(this IServiceCollection services) { - var settingsManager = provider.GetRequiredService(); - var settings = Task.Run(() => settingsManager.Load(name)).Result; - if (settings.Upgrade()) + var savedSettingsLocation = KnownFolders.WabbajackAppLocal.Combine("saved_settings"); + savedSettingsLocation.CreateDirectory(); + + services.AddSingleton(s => new Configuration { - settingsManager.Save(MainSettings.SettingsFileName, settings).FireAndForget(); - } + EncryptedDataLocation = KnownFolders.WabbajackAppLocal.Combine("encrypted"), + ModListsDownloadLocation = KnownFolders.EntryPoint.Combine("downloaded_mod_lists"), + SavedSettingsLocation = savedSettingsLocation, + LogLocation = KnownFolders.LauncherAwarePath.Combine("logs"), + ImageCacheLocation = KnownFolders.WabbajackAppLocal.Combine("image_cache") + }); - return settings; + return services; + } + + public static IServiceCollection AddSettings(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(s => s.GetRequiredService().GetAppSettings(s, MainSettings.SettingsFileName)); + services.AddSingleton(); + return services; } private static void CleanAllTempData(AbsolutePath path) diff --git a/Wabbajack.Services.OSIntegrated/SettingsManager.cs b/Wabbajack.Services.OSIntegrated/SettingsManager.cs index ca04ff765..dc52dd4af 100644 --- a/Wabbajack.Services.OSIntegrated/SettingsManager.cs +++ b/Wabbajack.Services.OSIntegrated/SettingsManager.cs @@ -5,29 +5,36 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Wabbajack.Common; +using Wabbajack.Configuration; using Wabbajack.DTOs.JsonConverters; using Wabbajack.Paths; using Wabbajack.Paths.IO; namespace Wabbajack.Services.OSIntegrated; -public class SettingsManager +public interface ISettingsManager { - private readonly Configuration _configuration; - private readonly DTOSerializer _dtos; - private readonly ILogger _logger; + Task Save(string key, T value); + Task Load(string key) where T : new(); + MainSettings GetAppSettings(IServiceProvider provider, string name); +} - public SettingsManager(ILogger logger, Configuration configuration, DTOSerializer dtos) +internal class SettingsManager(ILogger _logger, Configuration _configuration, DTOSerializer _dtos) : ISettingsManager +{ + private AbsolutePath GetPath(string key) { - _logger = logger; - _dtos = dtos; - _configuration = configuration; - _configuration.SavedSettingsLocation.CreateDirectory(); + return _configuration.SavedSettingsLocation.Combine(key).WithExtension(Ext.Json); } - private AbsolutePath GetPath(string key) + public MainSettings GetAppSettings(IServiceProvider provider, string name) { - return _configuration.SavedSettingsLocation.Combine(key).WithExtension(Ext.Json); + var settings = Task.Run(() => Load(name)).Result; + if (settings.Upgrade()) + { + Save(MainSettings.SettingsFileName, settings).FireAndForget(); + } + + return settings; } public async Task Save(string key, T value) diff --git a/Wabbajack.Services.OSIntegrated/Wabbajack.Services.OSIntegrated.csproj b/Wabbajack.Services.OSIntegrated/Wabbajack.Services.OSIntegrated.csproj index 55e7bfb54..39b248fc8 100644 --- a/Wabbajack.Services.OSIntegrated/Wabbajack.Services.OSIntegrated.csproj +++ b/Wabbajack.Services.OSIntegrated/Wabbajack.Services.OSIntegrated.csproj @@ -18,10 +18,12 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/Wabbajack.sln b/Wabbajack.sln index bedde649f..642f5ab20 100644 --- a/Wabbajack.sln +++ b/Wabbajack.sln @@ -108,8 +108,8 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".solutionItems", ".solutionItems", "{109037C8-CF2F-4179-B064-A66147BC18C5}" ProjectSection(SolutionItems) = preProject .gitignore = .gitignore - nuget.config = nuget.config CHANGELOG.md = CHANGELOG.md + nuget.config = nuget.config README.md = README.md EndProjectSection EndProject @@ -147,6 +147,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wabbajack.Downloaders.Verif EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wabbajack.Configuration", "Wabbajack.Configuration\Wabbajack.Configuration.csproj", "{E7CDACA6-D3FF-4CF6-8EF8-05FCD27F6FBE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wabbajack.Downloader.Services", "Wabbajack.Downloader.Clients\Wabbajack.Downloader.Services.csproj", "{258D44F2-956F-43A3-BD29-11A28D03F406}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -401,6 +403,10 @@ Global {E7CDACA6-D3FF-4CF6-8EF8-05FCD27F6FBE}.Debug|Any CPU.Build.0 = Debug|Any CPU {E7CDACA6-D3FF-4CF6-8EF8-05FCD27F6FBE}.Release|Any CPU.ActiveCfg = Release|Any CPU {E7CDACA6-D3FF-4CF6-8EF8-05FCD27F6FBE}.Release|Any CPU.Build.0 = Release|Any CPU + {258D44F2-956F-43A3-BD29-11A28D03F406}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {258D44F2-956F-43A3-BD29-11A28D03F406}.Debug|Any CPU.Build.0 = Debug|Any CPU + {258D44F2-956F-43A3-BD29-11A28D03F406}.Release|Any CPU.ActiveCfg = Release|Any CPU + {258D44F2-956F-43A3-BD29-11A28D03F406}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -451,6 +457,7 @@ Global {7FC4F129-F0FA-46B7-B7C4-532E371A6326} = {98B731EE-4FC0-4482-A069-BCBA25497871} {E4BDB22D-11A4-452F-8D10-D9CA9777EA22} = {F677890D-5109-43BC-97C7-C4CD47C8EE0C} {D9560C73-4E58-4463-9DB9-D06491E0E1C8} = {98B731EE-4FC0-4482-A069-BCBA25497871} + {258D44F2-956F-43A3-BD29-11A28D03F406} = {98B731EE-4FC0-4482-A069-BCBA25497871} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0AA30275-0F38-4A7D-B645-F5505178DDE8}