From 7443a0b3f30e3fa36bb85fbd93682fbff81815ee Mon Sep 17 00:00:00 2001 From: Timothy Baldridge Date: Fri, 31 Dec 2021 15:00:03 -0700 Subject: [PATCH] Can install SMEFT and get a "complete screen" and a log at the end --- Wabbajack.App.Wpf/App.xaml.cs | 4 +- .../Error States/ErrorResponse.cs | 2 +- Wabbajack.App.Wpf/LoadingLock.cs | 4 + .../Messages/LoadModlistForInstalling.cs | 10 +- Wabbajack.App.Wpf/Models/LoggerProvider.cs | 151 ++++++++++++++++++ .../View Models/Gallery/ModListMetadataVM.cs | 2 +- .../View Models/Installers/InstallerVM.cs | 77 ++++++++- Wabbajack.App.Wpf/View Models/MainWindowVM.cs | 2 +- Wabbajack.App.Wpf/Views/Common/LogView.xaml | 4 +- .../Views/Installers/InstallationView.xaml.cs | 21 ++- Wabbajack.Installer/StandardInstaller.cs | 15 ++ 11 files changed, 272 insertions(+), 20 deletions(-) create mode 100644 Wabbajack.App.Wpf/Models/LoggerProvider.cs diff --git a/Wabbajack.App.Wpf/App.xaml.cs b/Wabbajack.App.Wpf/App.xaml.cs index 27019941..d1bef5c6 100644 --- a/Wabbajack.App.Wpf/App.xaml.cs +++ b/Wabbajack.App.Wpf/App.xaml.cs @@ -11,6 +11,7 @@ using ReactiveUI; using Splat; using Wabbajack.Common; using Wabbajack; +using Wabbajack.DTOs; using Wabbajack.Models; using Wabbajack.Services.OSIntegrated; using Wabbajack.Util; @@ -40,7 +41,7 @@ namespace Wabbajack _serviceProvider = _host.Services; } - private IServiceCollection ConfigureServices(IServiceCollection services) + private static IServiceCollection ConfigureServices(IServiceCollection services) { RxApp.MainThreadScheduler = new DispatcherScheduler(Dispatcher.CurrentDispatcher); @@ -50,6 +51,7 @@ namespace Wabbajack services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddAllSingleton(); services.AddSingleton(); services.AddTransient(); diff --git a/Wabbajack.App.Wpf/Error States/ErrorResponse.cs b/Wabbajack.App.Wpf/Error States/ErrorResponse.cs index 871e5507..9bdc5a82 100644 --- a/Wabbajack.App.Wpf/Error States/ErrorResponse.cs +++ b/Wabbajack.App.Wpf/Error States/ErrorResponse.cs @@ -5,7 +5,7 @@ namespace Wabbajack public struct ErrorResponse : IErrorResponse { public static readonly ErrorResponse Success = Succeed(); - public static readonly ErrorResponse Failure = new ErrorResponse(); + public static readonly ErrorResponse Failure = new(); public bool Succeeded { get; } public Exception? Exception { get; } diff --git a/Wabbajack.App.Wpf/LoadingLock.cs b/Wabbajack.App.Wpf/LoadingLock.cs index a6bea681..b7aca2c3 100644 --- a/Wabbajack.App.Wpf/LoadingLock.cs +++ b/Wabbajack.App.Wpf/LoadingLock.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Disposables; +using System.Reactive.Linq; using ReactiveUI; using ReactiveUI.Fody.Helpers; @@ -9,6 +10,7 @@ public class LoadingLock : ReactiveObject, IDisposable { private readonly CompositeDisposable _disposable; + [Reactive] public ErrorResponse? ErrorState { get; set; } public LoadingLock() @@ -16,10 +18,12 @@ public class LoadingLock : ReactiveObject, IDisposable _disposable = new CompositeDisposable(); this.WhenAnyValue(vm => vm.LoadLevel) + .StartWith(0) .Subscribe(v => IsLoading = v > 0) .DisposeWith(_disposable); this.WhenAnyValue(vm => vm.LoadLevel) + .StartWith(0) .Subscribe(v => IsNotLoading = v == 0) .DisposeWith(_disposable); } diff --git a/Wabbajack.App.Wpf/Messages/LoadModlistForInstalling.cs b/Wabbajack.App.Wpf/Messages/LoadModlistForInstalling.cs index b470d255..bba1c98b 100644 --- a/Wabbajack.App.Wpf/Messages/LoadModlistForInstalling.cs +++ b/Wabbajack.App.Wpf/Messages/LoadModlistForInstalling.cs @@ -1,4 +1,5 @@ using ReactiveUI; +using Wabbajack.DTOs; using Wabbajack.Paths; namespace Wabbajack.Messages; @@ -6,14 +7,17 @@ namespace Wabbajack.Messages; public class LoadModlistForInstalling { public AbsolutePath Path { get; } + + public ModlistMetadata Metadata { get; } - public LoadModlistForInstalling(AbsolutePath path) + public LoadModlistForInstalling(AbsolutePath path, ModlistMetadata metadata) { Path = path; + Metadata = metadata; } - public static void Send(AbsolutePath path) + public static void Send(AbsolutePath path, ModlistMetadata metadata) { - MessageBus.Current.SendMessage(new LoadModlistForInstalling(path)); + MessageBus.Current.SendMessage(new LoadModlistForInstalling(path, metadata)); } } \ No newline at end of file diff --git a/Wabbajack.App.Wpf/Models/LoggerProvider.cs b/Wabbajack.App.Wpf/Models/LoggerProvider.cs new file mode 100644 index 00000000..c486a509 --- /dev/null +++ b/Wabbajack.App.Wpf/Models/LoggerProvider.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.IO; +using System.Reactive.Disposables; +using System.Reactive.Subjects; +using System.Text; +using System.Threading; +using DynamicData; +using Microsoft.Extensions.Logging; +using Wabbajack.Paths; +using Wabbajack.Paths.IO; +using Wabbajack.Services.OSIntegrated; + +namespace Wabbajack.Models; + +public class LoggerProvider : ILoggerProvider +{ + private readonly RelativePath _appName; + private readonly Configuration _configuration; + private readonly CompositeDisposable _disposables; + private readonly Stream _logFile; + private readonly StreamWriter _logStream; + + public readonly ReadOnlyObservableCollection _messagesFiltered; + private readonly DateTime _startupTime; + + private long _messageId; + private readonly SourceCache _messageLog = new(m => m.MessageId); + private readonly Subject _messages = new(); + + public LoggerProvider(Configuration configuration) + { + _startupTime = DateTime.UtcNow; + _configuration = configuration; + _configuration.LogLocation.CreateDirectory(); + + _disposables = new CompositeDisposable(); + + Messages.Subscribe(m => _messageLog.AddOrUpdate(m)) + .DisposeWith(_disposables); + + Messages.Subscribe(m => LogToFile(m)) + .DisposeWith(_disposables); + + _messageLog.Connect() + .Bind(out _messagesFiltered) + .Subscribe() + .DisposeWith(_disposables); + + _messages.DisposeWith(_disposables); + + _appName = typeof(LoggerProvider).Assembly.Location.ToAbsolutePath().FileName; + LogPath = _configuration.LogLocation.Combine($"{_appName}.current.log"); + _logFile = LogPath.Open(FileMode.Append, FileAccess.Write); + _logFile.DisposeWith(_disposables); + + _logStream = new StreamWriter(_logFile, Encoding.UTF8); + } + + public IObservable Messages => _messages; + public AbsolutePath LogPath { get; } + public ReadOnlyObservableCollection MessageLog => _messagesFiltered; + + public void Dispose() + { + _disposables.Dispose(); + } + + public ILogger CreateLogger(string categoryName) + { + return new Logger(this, categoryName); + } + + private void LogToFile(ILogMessage logMessage) + { + var line = $"[{logMessage.TimeStamp - _startupTime}] {logMessage.LongMessage}"; + lock (_logStream) + { + _logStream.Write(line); + _logStream.Flush(); + } + } + + private long NextMessageId() + { + return Interlocked.Increment(ref _messageId); + } + + public class Logger : ILogger + { + private readonly string _categoryName; + private readonly LoggerProvider _provider; + private ImmutableList Scopes = ImmutableList.Empty; + + public Logger(LoggerProvider provider, string categoryName) + { + _categoryName = categoryName; + _provider = provider; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, + Func formatter) + { + _provider._messages.OnNext(new LogMessage(DateTime.UtcNow, _provider.NextMessageId(), logLevel, + eventId, state, exception, formatter)); + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public IDisposable BeginScope(TState state) + { + Scopes = Scopes.Add(state); + return Disposable.Create(() => Scopes = Scopes.Remove(state)); + } + } + + public interface ILogMessage + { + long MessageId { get; } + + string ShortMessage { get; } + DateTime TimeStamp { get; } + string LongMessage { get; } + } + + private record LogMessage(DateTime TimeStamp, long MessageId, LogLevel LogLevel, EventId EventId, + TState State, Exception? Exception, Func Formatter) : ILogMessage + { + public string ShortMessage => Formatter(State, Exception); + + public string LongMessage + { + get + { + var sb = new StringBuilder(); + sb.AppendLine(ShortMessage); + if (Exception != null) + { + sb.Append("Exception: "); + sb.Append(Exception); + } + + return sb.ToString(); + } + } + } +} \ No newline at end of file diff --git a/Wabbajack.App.Wpf/View Models/Gallery/ModListMetadataVM.cs b/Wabbajack.App.Wpf/View Models/Gallery/ModListMetadataVM.cs index d54e800d..bd0162a4 100644 --- a/Wabbajack.App.Wpf/View Models/Gallery/ModListMetadataVM.cs +++ b/Wabbajack.App.Wpf/View Models/Gallery/ModListMetadataVM.cs @@ -147,7 +147,7 @@ namespace Wabbajack { if (await _maintainer.HaveModList(Metadata)) { - LoadModlistForInstalling.Send(_maintainer.ModListPath(Metadata)); + LoadModlistForInstalling.Send(_maintainer.ModListPath(Metadata), Metadata); NavigateToGlobal.Send(NavigateToGlobal.ScreenType.Installer); } else diff --git a/Wabbajack.App.Wpf/View Models/Installers/InstallerVM.cs b/Wabbajack.App.Wpf/View Models/Installers/InstallerVM.cs index 7e18b90e..fc0bbba8 100644 --- a/Wabbajack.App.Wpf/View Models/Installers/InstallerVM.cs +++ b/Wabbajack.App.Wpf/View Models/Installers/InstallerVM.cs @@ -9,12 +9,14 @@ using DynamicData; using DynamicData.Binding; using System.Reactive; using System.Text; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.WindowsAPICodePack.Dialogs; using Microsoft.WindowsAPICodePack.Shell; using Wabbajack.Common; +using Wabbajack.Downloaders.GameFile; using Wabbajack.DTOs; using Wabbajack.DTOs.JsonConverters; using Wabbajack.Extensions; @@ -22,11 +24,13 @@ using Wabbajack.Hashing.xxHash64; using Wabbajack.Installer; using Wabbajack.Interventions; using Wabbajack.Messages; +using Wabbajack.Models; using Wabbajack.Paths; using Wabbajack.RateLimiter; using Wabbajack.View_Models; using Wabbajack.Paths.IO; using Wabbajack.Services.OSIntegrated; +using Wabbajack.Util; using Consts = Wabbajack.Consts; using KnownFolders = Wabbajack.Paths.IO.KnownFolders; @@ -37,6 +41,14 @@ public enum ModManager Standard } +public enum InstallState +{ + Configuration, + Installing, + Success, + Failure +} + public class InstallerVM : BackNavigatingVM, IBackNavigatingVM { private const string LastLoadedModlist = "last-loaded-modlist"; @@ -45,6 +57,9 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM [Reactive] public ModList ModList { get; set; } + [Reactive] + public ModlistMetadata ModlistMetadata { get; set; } + [Reactive] public ErrorResponse? Completed { get; set; } @@ -60,7 +75,10 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM [Reactive] public BitmapFrame SlideShowImage { get; set; } - + + + [Reactive] + public InstallState InstallState { get; set; } /// /// Slideshow Data @@ -79,10 +97,17 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM private readonly DTOSerializer _dtos; private readonly ILogger _logger; private readonly SettingsManager _settingsManager; + private readonly IServiceProvider _serviceProvider; + private readonly SystemParametersConstructor _parametersConstructor; + private readonly IGameLocator _gameLocator; + private readonly LoggerProvider _loggerProvider; [Reactive] public bool Installing { get; set; } + [Reactive] + public LoggerProvider LoggerProvider { get; set; } + // Command properties public ReactiveCommand ShowManifestCommand { get; } @@ -96,11 +121,17 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM public ReactiveCommand BackCommand { get; } - public InstallerVM(ILogger logger, DTOSerializer dtos, SettingsManager settingsManager) : base(logger) + public InstallerVM(ILogger logger, DTOSerializer dtos, SettingsManager settingsManager, IServiceProvider serviceProvider, + SystemParametersConstructor parametersConstructor, IGameLocator gameLocator, LoggerProvider loggerProvider) : base(logger) { _logger = logger; + LoggerProvider = loggerProvider; _settingsManager = settingsManager; _dtos = dtos; + _serviceProvider = serviceProvider; + _parametersConstructor = parametersConstructor; + _gameLocator = gameLocator; + Installer = new MO2InstallerVM(this); BackCommand = ReactiveCommand.Create(() => NavigateToGlobal.Send(NavigateToGlobal.ScreenType.ModeSelectionView)); @@ -126,7 +157,7 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM ModListLocation.Filters.Add(new CommonFileDialogFilter("Wabbajack Modlist", "*.wabbajack")); MessageBus.Current.Listen() - .Subscribe(msg => LoadModlist(msg.Path).FireAndForget()) + .Subscribe(msg => LoadModlist(msg.Path, msg.Metadata).FireAndForget()) .DisposeWith(CompositeDisposable); MessageBus.Current.Listen() @@ -137,8 +168,10 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM this.WhenActivated(disposables => { + + ModListLocation.WhenAnyValue(l => l.TargetPath) - .Subscribe(p => LoadModlist(p).FireAndForget()) + .Subscribe(p => LoadModlist(p, null).FireAndForget()) .DisposeWith(disposables); }); @@ -149,12 +182,13 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM { var lst = await _settingsManager.Load(LastLoadedModlist); if (lst.FileExists()) - await LoadModlist(lst); + await LoadModlist(lst, null); } - private async Task LoadModlist(AbsolutePath path) + private async Task LoadModlist(AbsolutePath path, ModlistMetadata? metadata) { using var ll = LoadingLock.WithLoading(); + InstallState = InstallState.Configuration; ModListLocation.TargetPath = path; try { @@ -169,6 +203,7 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM ModListLocation.TargetPath = prevSettings.ModListLocation; Installer.Location.TargetPath = prevSettings.InstallLocation; Installer.DownloadLocation.TargetPath = prevSettings.DownloadLoadction; + ModlistMetadata = metadata ?? prevSettings.Metadata; } PopulateSlideShow(ModList); @@ -183,16 +218,40 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM } } - public async Task BeginInstall() + private async Task BeginInstall() { + InstallState = InstallState.Installing; var postfix = (await ModListLocation.TargetPath.ToString().Hash()).ToHex(); await _settingsManager.Save(InstallSettingsPrefix + postfix, new SavedInstallSettings { ModListLocation = ModListLocation.TargetPath, InstallLocation = Installer.Location.TargetPath, - DownloadLoadction = Installer.DownloadLocation.TargetPath + DownloadLoadction = Installer.DownloadLocation.TargetPath, + Metadata = ModlistMetadata }); + try + { + var installer = StandardInstaller.Create(_serviceProvider, new InstallerConfiguration + { + Game = ModList.GameType, + Downloads = Installer.DownloadLocation.TargetPath, + Install = Installer.Location.TargetPath, + ModList = ModList, + ModlistArchive = ModListLocation.TargetPath, + SystemParameters = _parametersConstructor.Create(), + GameFolder = _gameLocator.GameLocation(ModList.GameType) + }); + + await installer.Begin(CancellationToken.None); + + InstallState = InstallState.Success; + } + catch (Exception ex) + { + InstallState = InstallState.Failure; + } + } class SavedInstallSettings @@ -200,6 +259,8 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM public AbsolutePath ModListLocation { get; set; } public AbsolutePath InstallLocation { get; set; } public AbsolutePath DownloadLoadction { get; set; } + + public ModlistMetadata Metadata { get; set; } } private void PopulateSlideShow(ModList modList) diff --git a/Wabbajack.App.Wpf/View Models/MainWindowVM.cs b/Wabbajack.App.Wpf/View Models/MainWindowVM.cs index b272c406..b48b3fa3 100644 --- a/Wabbajack.App.Wpf/View Models/MainWindowVM.cs +++ b/Wabbajack.App.Wpf/View Models/MainWindowVM.cs @@ -134,7 +134,7 @@ namespace Wabbajack if (IsStartingFromModlist(out var path)) { - LoadModlistForInstalling.Send(path); + LoadModlistForInstalling.Send(path, null); NavigateToGlobal.Send(NavigateToGlobal.ScreenType.Installer); } else diff --git a/Wabbajack.App.Wpf/Views/Common/LogView.xaml b/Wabbajack.App.Wpf/Views/Common/LogView.xaml index 4e340afc..3b21e9c9 100644 --- a/Wabbajack.App.Wpf/Views/Common/LogView.xaml +++ b/Wabbajack.App.Wpf/Views/Common/LogView.xaml @@ -14,11 +14,11 @@ local:AutoScrollBehavior.ScrollOnNewItem="True" BorderBrush="Transparent" BorderThickness="1" - ItemsSource="{Binding Log}" + ItemsSource="{Binding LoggerProvider.MessageLog}" ScrollViewer.HorizontalScrollBarVisibility="Disabled"> - + diff --git a/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml.cs b/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml.cs index 230c06b0..53631651 100644 --- a/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml.cs @@ -16,9 +16,24 @@ namespace Wabbajack InitializeComponent(); this.WhenActivated(disposables => { - MidInstallDisplayGrid.Visibility = Visibility.Collapsed; - LogView.Visibility = Visibility.Collapsed; - CpuView.Visibility = Visibility.Collapsed; + //MidInstallDisplayGrid.Visibility = Visibility.Collapsed; + //LogView.Visibility = Visibility.Collapsed; + //CpuView.Visibility = Visibility.Collapsed; + + ViewModel.WhenAnyValue(vm => vm.InstallState) + .Select(v => v != InstallState.Configuration ? Visibility.Visible : Visibility.Collapsed) + .BindToStrict(this, view => view.MidInstallDisplayGrid.Visibility) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(vm => vm.InstallState) + .Select(v => v == InstallState.Configuration ? Visibility.Visible : Visibility.Collapsed) + .BindToStrict(this, view => view.BottomButtonInputGrid.Visibility) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(vm => vm.InstallState) + .Select(es => es == InstallState.Success ? Visibility.Visible : Visibility.Collapsed) + .BindToStrict(this, view => view.InstallComplete.Visibility) + .DisposeWith(disposables); //ViewModel.WhenAnyValue(vm => vm.ModList.Name) // .BindToStrict(this, view => view.Name) diff --git a/Wabbajack.Installer/StandardInstaller.cs b/Wabbajack.Installer/StandardInstaller.cs index 4240b2be..82f732cb 100644 --- a/Wabbajack.Installer/StandardInstaller.cs +++ b/Wabbajack.Installer/StandardInstaller.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using IniParser; using IniParser.Model.Configuration; using IniParser.Parser; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Wabbajack.Common; using Wabbajack.Compression.BSA; @@ -41,6 +42,20 @@ public class StandardInstaller : AInstaller MaxSteps = 14; } + public static StandardInstaller Create(IServiceProvider provider, InstallerConfiguration configuration) + { + return new StandardInstaller(provider.GetRequiredService>(), + configuration, + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService()); + } + public override async Task Begin(CancellationToken token) { if (token.IsCancellationRequested) return false;