Merge branch 'main' into upgrade-deps

This commit is contained in:
Timothy Baldridge 2021-11-04 20:44:33 -06:00 committed by GitHub
commit 8e792e6092
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 292 additions and 155 deletions

View File

@ -1,8 +1,10 @@
using System; using System;
using System.Threading;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -24,6 +26,7 @@ public class App : Application
public override void Initialize() public override void Initialize()
{ {
Dispatcher.UIThread.Post(() => Thread.CurrentThread.Name = "UIThread");
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }

View File

@ -37,7 +37,31 @@
<Style Selector="Border.Settings"> <Style Selector="Border.Settings">
<Setter Property="BorderThickness" Value="2"></Setter> <Setter Property="BorderThickness" Value="2"></Setter>
<Setter Property="BorderBrush" Value="DarkGray"></Setter>
<Setter Property="CornerRadius" Value="4"></Setter>
</Style> </Style>
<Style Selector="Border.Settings Grid">
<Setter Property="Margin" Value="4"></Setter>
</Style>
<Style Selector="Grid.LogView ItemsControl">
<Setter Property="Margin" Value="4"></Setter>
</Style>
<Style Selector="Grid.LogView > TextBlock.Title">
<Setter Property="FontWeight" Value="Bold"></Setter>
<Setter Property="FontSize" Value="18"></Setter>
<Setter Property="Margin" Value="4"/>
</Style>
<Style Selector="Grid.LogView > Border">
<Setter Property="Margin" Value="4"/>
<Setter Property="BorderThickness" Value="2"></Setter>
<Setter Property="BorderBrush" Value="DarkGray"></Setter>
<Setter Property="CornerRadius" Value="4"></Setter>
</Style>
</Styles> </Styles>

View File

@ -30,7 +30,7 @@ public partial class FileSelectionBox : ReactiveUserControl<FileSelectionBoxView
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
this.Bind(ViewModel, vm => vm.Path, view => view.SelectedPath) this.OneWayBind(ViewModel, vm => vm.Path, view => view.SelectedPath)
.DisposeWith(disposables); .DisposeWith(disposables);
this.WhenAnyValue(view => view.SelectFolder) this.WhenAnyValue(view => view.SelectFolder)
.BindTo(ViewModel, vm => vm.SelectFolder) .BindTo(ViewModel, vm => vm.SelectFolder)
@ -41,7 +41,7 @@ public partial class FileSelectionBox : ReactiveUserControl<FileSelectionBoxView
exts.Split("|", StringSplitOptions.RemoveEmptyEntries).Select(s => new Extension(s)).ToArray()) exts.Split("|", StringSplitOptions.RemoveEmptyEntries).Select(s => new Extension(s)).ToArray())
.BindTo(ViewModel, vm => vm.Extensions) .BindTo(ViewModel, vm => vm.Extensions)
.DisposeWith(disposables); .DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Path, this.OneWayBind(ViewModel, vm => vm.Path,
view => view.TextBox.Text) view => view.TextBox.Text)
.DisposeWith(disposables); .DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.BrowseCommand, this.BindCommand(ViewModel, vm => vm.BrowseCommand,
@ -67,4 +67,9 @@ public partial class FileSelectionBox : ReactiveUserControl<FileSelectionBoxView
get => GetValue(SelectFolderProperty); get => GetValue(SelectFolderProperty);
set => SetValue(SelectFolderProperty, value); set => SetValue(SelectFolderProperty, value);
} }
public void Load(AbsolutePath path)
{
ViewModel.Path = path;
}
} }

View File

@ -6,23 +6,25 @@
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Wabbajack.App.Controls.LogView"> x:Class="Wabbajack.App.Controls.LogView">
<Grid RowDefinitions="Auto, *, Auto"> <Grid RowDefinitions="Auto, *, Auto" Classes="LogView">
<TextBlock Grid.Row="0">Current Log Contents</TextBlock> <TextBlock Grid.Row="0" Classes="Title">Current Log Contents</TextBlock>
<ScrollViewer Grid.Row="1" ScrollChanged="ScrollViewer_OnScrollChanged" x:Name="ScrollViewer" <Border Grid.Row="1">
HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible"> <ScrollViewer ScrollChanged="ScrollViewer_OnScrollChanged" x:Name="ScrollViewer"
<ItemsControl x:Name="Messages"> HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible">
<ItemsControl.ItemsPanel> <ItemsControl x:Name="Messages">
<ItemsPanelTemplate> <ItemsControl.ItemsPanel>
<StackPanel /> <ItemsPanelTemplate>
</ItemsPanelTemplate> <StackPanel />
</ItemsControl.ItemsPanel> </ItemsPanelTemplate>
<ItemsControl.ItemTemplate> </ItemsControl.ItemsPanel>
<DataTemplate> <ItemsControl.ItemTemplate>
<controls:LogViewItem /> <DataTemplate>
</DataTemplate> <controls:LogViewItem />
</ItemsControl.ItemTemplate> </DataTemplate>
</ItemsControl> </ItemsControl.ItemTemplate>
</ScrollViewer> </ItemsControl>
</ScrollViewer>
</Border>
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right"> <StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="CopyLog"> <Button x:Name="CopyLog">
<avalonia:MaterialIcon Kind="ContentCopy" /> <avalonia:MaterialIcon Kind="ContentCopy" />
@ -32,4 +34,5 @@
</Button> </Button>
</StackPanel> </StackPanel>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@ -1,24 +1,43 @@
using System; using System;
using System.Linq.Expressions;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using ReactiveUI;
namespace Wabbajack.App.Extensions; namespace Wabbajack.App.Extensions;
public static class IObservableExtensions public static class IObservableExtensions
{ {
public static IObservable<TOut> SelectAsync<TIn, TOut>(this IObservable<TIn> input, public static IDisposable SimpleOneWayBind<TView, TViewModel, TProp, TOut>(
CompositeDisposable disposable, this TView view,
Func<TIn, ValueTask<TOut>> func) TViewModel? viewModel,
Expression<Func<TViewModel, TProp?>> vmProperty,
Expression<Func<TView, TOut?>> viewProperty)
where TView : class
{ {
Subject<TOut> returnObs = new(); var d = viewModel.WhenAny(vmProperty, change => change.Value)
.ObserveOn(RxApp.MainThreadScheduler)
.BindTo(view, viewProperty);
input.Subscribe(x => Task.Run(async () => return Disposable.Create(() => d.Dispose());
{ }
var result = await func(x);
returnObs.OnNext(result);
})).DisposeWith(disposable);
return returnObs; public static IDisposable SimpleOneWayBind<TView, TViewModel, TProp, TOut>(
this TView view,
TViewModel? viewModel,
Expression<Func<TViewModel, TProp?>> vmProperty,
Expression<Func<TView, TOut?>> viewProperty,
Func<TProp?, TOut> selector)
where TView : class
{
var d = viewModel.WhenAnyValue(vmProperty)
.Select(change => selector(change))
.ObserveOn(RxApp.MainThreadScheduler)
.BindTo(view, viewProperty);
return Disposable.Create(() => d.Dispose());
} }
} }

View File

@ -1,5 +1,14 @@
using System;
using System.Reactive.Linq;
using Avalonia.Threading;
using ReactiveUI;
namespace Wabbajack.App.Extensions; namespace Wabbajack.App.Extensions;
public static class ReactiveUIExtensions public static class ReactiveUIExtensions
{ {
public static IObservable<T> OnUIThread<T>(this IObservable<T> src)
{
return src.ObserveOn(AvaloniaScheduler.Instance);
}
} }

View File

@ -5,4 +5,5 @@ namespace Wabbajack.App.Interfaces;
public interface IScreenView public interface IScreenView
{ {
public Type ViewModelType { get; } public Type ViewModelType { get; }
public string HumanName { get; }
} }

View File

@ -48,10 +48,12 @@ public class SettingsManager
try try
{ {
if (path.FileExists()) if (path.FileExists())
{
await using (var s = path.Open(FileMode.Open)) await using (var s = path.Open(FileMode.Open))
{ {
return (await JsonSerializer.DeserializeAsync<T>(s, _dtos.Options))!; return (await JsonSerializer.DeserializeAsync<T>(s, _dtos.Options))!;
} }
}
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -6,7 +6,7 @@ namespace Wabbajack.App.Screens;
public partial class BrowseView : ScreenBase<BrowseViewModel> public partial class BrowseView : ScreenBase<BrowseViewModel>
{ {
public BrowseView() public BrowseView() : base("Web Browser")
{ {
InitializeComponent(); InitializeComponent();
this.WhenActivated(disposables => this.WhenActivated(disposables =>

View File

@ -6,7 +6,7 @@ namespace Wabbajack.App.Screens;
public partial class CompilationView : ScreenBase<CompilationViewModel> public partial class CompilationView : ScreenBase<CompilationViewModel>
{ {
public CompilationView() public CompilationView() : base("Compiling")
{ {
InitializeComponent(); InitializeComponent();

View File

@ -15,7 +15,7 @@ namespace Wabbajack.App.Screens;
public partial class CompilerConfigurationView : ScreenBase<CompilerConfigurationViewModel> public partial class CompilerConfigurationView : ScreenBase<CompilerConfigurationViewModel>
{ {
public CompilerConfigurationView() public CompilerConfigurationView() : base("Compiler Configuration")
{ {
InitializeComponent(); InitializeComponent();
AddAlwaysEnabled.Command = ReactiveCommand.Create(() => AddAlwaysEnabled_Command().FireAndForget()); AddAlwaysEnabled.Command = ReactiveCommand.Create(() => AddAlwaysEnabled_Command().FireAndForget());

View File

@ -6,7 +6,7 @@ namespace Wabbajack.App.Screens;
public partial class ErrorPageView : ScreenBase<ErrorPageViewModel> public partial class ErrorPageView : ScreenBase<ErrorPageViewModel>
{ {
public ErrorPageView() public ErrorPageView() : base("Error")
{ {
InitializeComponent(); InitializeComponent();
this.WhenActivated(disposables => this.WhenActivated(disposables =>

View File

@ -6,7 +6,7 @@ namespace Wabbajack.App.Screens;
public partial class LauncherView : ScreenBase<LauncherViewModel> public partial class LauncherView : ScreenBase<LauncherViewModel>
{ {
public LauncherView() public LauncherView() : base("Launch Modlist")
{ {
InitializeComponent(); InitializeComponent();
this.WhenActivated(disposables => this.WhenActivated(disposables =>

View File

@ -35,20 +35,20 @@ public class LauncherViewModel : ViewModelBase
_logger = logger; _logger = logger;
MessageBus.Current.Listen<ConfigureLauncher>() MessageBus.Current.Listen<ConfigureLauncher>()
.Subscribe(v => Receive(v)) .Subscribe(Receive)
.DisposeWith(VMDisposables); .DisposeWith(VMDisposables);
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
this.WhenAnyValue(v => v.InstallFolder) this.WhenAnyValue(v => v.InstallFolder)
.SelectAsync(disposables, async folder => await manager.GetByInstallFolder(folder)) .SelectMany(async folder => await manager.GetByInstallFolder(folder))
.ObserveOn(RxApp.MainThreadScheduler) .OnUIThread()
.Where(v => v != null) .Where(v => v != null)
.BindTo(this, vm => vm.Setting) .BindTo(this, vm => vm.Setting)
.DisposeWith(disposables); .DisposeWith(disposables);
this.WhenAnyValue(v => v.Setting) this.WhenAnyValue(v => v.Setting)
.Where(v => v != default) .Where(v => v != default && v!.Image != default && v!.Image.FileExists())
.Select(v => new Bitmap(v!.Image.ToString())) .Select(v => new Bitmap(v!.Image.ToString()))
.BindTo(this, vm => vm.Image) .BindTo(this, vm => vm.Image)
.DisposeWith(disposables); .DisposeWith(disposables);

View File

@ -4,7 +4,7 @@ namespace Wabbajack.App.Screens;
public partial class LogScreenView : ScreenBase<LogScreenViewModel> public partial class LogScreenView : ScreenBase<LogScreenViewModel>
{ {
public LogScreenView() public LogScreenView() : base("Application Log")
{ {
InitializeComponent(); InitializeComponent();
} }

View File

@ -6,7 +6,7 @@ namespace Wabbajack.App.Screens;
public partial class PlaySelectView : ScreenBase<PlaySelectViewModel> public partial class PlaySelectView : ScreenBase<PlaySelectViewModel>
{ {
public PlaySelectView() public PlaySelectView() : base("Modlist Selection")
{ {
InitializeComponent(); InitializeComponent();
this.WhenActivated(disposables => this.WhenActivated(disposables =>

View File

@ -6,7 +6,7 @@ namespace Wabbajack.App.Screens;
public partial class SettingsView : ScreenBase<SettingsViewModel> public partial class SettingsView : ScreenBase<SettingsViewModel>
{ {
public SettingsView() public SettingsView() : base("Settings")
{ {
InitializeComponent(); InitializeComponent();
this.WhenActivated(disposables => this.WhenActivated(disposables =>

View File

@ -7,8 +7,8 @@
x:Class="Wabbajack.App.Views.StandardInstallationView"> x:Class="Wabbajack.App.Views.StandardInstallationView">
<Grid RowDefinitions="40, 5, 5, *, 40"> <Grid RowDefinitions="40, 5, 5, *, 40">
<TextBlock Grid.Row="0" x:Name="StatusText" FontSize="20" FontWeight="Bold">[20/30] Installing Files</TextBlock> <TextBlock Grid.Row="0" x:Name="StatusText" FontSize="20" FontWeight="Bold">[20/30] Installing Files</TextBlock>
<ProgressBar Grid.Row="1" x:Name="StepsProgress" Maximum="1000" Value="40" /> <ProgressBar Grid.Row="1" x:Name="StepsProgress" Maximum="1000" Value="0" />
<ProgressBar Grid.Row="2" x:Name="StepProgress" Maximum="10000" Value="30" /> <ProgressBar Grid.Row="2" x:Name="StepProgress" Maximum="10000" Value="0" />
<Viewbox Grid.Row="3" HorizontalAlignment="Center" <Viewbox Grid.Row="3" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
Stretch="Uniform"> Stretch="Uniform">

View File

@ -1,18 +1,22 @@
using System;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Threading;
using ReactiveUI; using ReactiveUI;
using Wabbajack.App.Extensions;
using Wabbajack.App.ViewModels; using Wabbajack.App.ViewModels;
namespace Wabbajack.App.Views; namespace Wabbajack.App.Views;
public partial class StandardInstallationView : ScreenBase<StandardInstallationViewModel> public partial class StandardInstallationView : ScreenBase<StandardInstallationViewModel>
{ {
public StandardInstallationView() public StandardInstallationView() : base("Installing")
{ {
InitializeComponent(); InitializeComponent();
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
this.Bind(ViewModel, vm => vm.Slide.Image, view => view.SlideImage.Source) this.OneWayBind(ViewModel, vm => vm.Slide.Image, view => view.SlideImage.Source)
.DisposeWith(disposables); .DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.NextCommand, view => view.NextSlide) this.BindCommand(ViewModel, vm => vm.NextCommand, view => view.NextSlide)
@ -33,8 +37,9 @@ public partial class StandardInstallationView : ScreenBase<StandardInstallationV
this.OneWayBind(ViewModel, vm => vm.StepsProgress, view => view.StepsProgress.Value, p => p.Value * 1000) this.OneWayBind(ViewModel, vm => vm.StepsProgress, view => view.StepsProgress.Value, p => p.Value * 1000)
.DisposeWith(disposables); .DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.StepProgress, view => view.StepProgress.Value, p => p.Value * 10000) this.OneWayBind(ViewModel, vm => vm.StepProgress, p => p.StepProgress.Value, p => p.Value * 10000)
.DisposeWith(disposables); .DisposeWith(disposables);
}); });
} }
} }

View File

@ -20,6 +20,7 @@ using Wabbajack.App.Utilities;
using Wabbajack.App.ViewModels.SubViewModels; using Wabbajack.App.ViewModels.SubViewModels;
using Wabbajack.Common; using Wabbajack.Common;
using Wabbajack.Downloaders.GameFile; using Wabbajack.Downloaders.GameFile;
using Wabbajack.Downloaders.Interfaces;
using Wabbajack.DTOs; using Wabbajack.DTOs;
using Wabbajack.DTOs.DownloadStates; using Wabbajack.DTOs.DownloadStates;
using Wabbajack.DTOs.JsonConverters; using Wabbajack.DTOs.JsonConverters;
@ -44,6 +45,7 @@ public class StandardInstallationViewModel : ViewModelBase
private IServiceScope _scope; private IServiceScope _scope;
private SlideViewModel[] _slides = Array.Empty<SlideViewModel>(); private SlideViewModel[] _slides = Array.Empty<SlideViewModel>();
private Timer _slideTimer; private Timer _slideTimer;
private Timer _updateTimer;
public StandardInstallationViewModel(ILogger<StandardInstallationViewModel> logger, IServiceProvider provider, public StandardInstallationViewModel(ILogger<StandardInstallationViewModel> logger, IServiceProvider provider,
GameLocator locator, DTOSerializer dtos, GameLocator locator, DTOSerializer dtos,
@ -63,6 +65,9 @@ public class StandardInstallationViewModel : ViewModelBase
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
_updateTimer = new Timer(UpdateStatus, null, TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(100));
_updateTimer.DisposeWith(disposables);
_slideTimer = new Timer(_ => _slideTimer = new Timer(_ =>
{ {
if (IsPlaying) NextSlide(1); if (IsPlaying) NextSlide(1);
@ -102,12 +107,24 @@ public class StandardInstallationViewModel : ViewModelBase
[Reactive] public string StatusText { get; set; } = ""; [Reactive] public string StatusText { get; set; } = "";
[Reactive] public Percent StepsProgress { get; set; } = Percent.Zero; [Reactive] public Percent StepsProgress { get; set; } = Percent.Zero;
[Reactive] public Percent StepProgress { get; set; } = Percent.Zero; [Reactive] public Percent StepProgress { get; set; } = Percent.Zero;
// Not Reactive, so we don't end up spamming the UI threads with events
public StatusUpdate _latestStatus = new("", Percent.Zero, Percent.Zero);
public void Receive(StartInstallation msg) public void Receive(StartInstallation msg)
{ {
Install(msg).FireAndForget(); Install(msg).FireAndForget();
} }
private void UpdateStatus(object? state)
{
Dispatcher.UIThread.Post(() =>
{
StepsProgress = _latestStatus.StepsProgress;
StepProgress = _latestStatus.StepProgress;
StatusText = _latestStatus.StatusText;
}, DispatcherPriority.Render);
}
private void NextSlide(int direction) private void NextSlide(int direction)
{ {
@ -175,21 +192,12 @@ public class StandardInstallationViewModel : ViewModelBase
_installer = _provider.GetService<StandardInstaller>()!; _installer = _provider.GetService<StandardInstaller>()!;
_installer.OnStatusUpdate = async update => _installer.OnStatusUpdate = update => _latestStatus = update;
{
Trace.TraceInformation("Update....");
await Dispatcher.UIThread.InvokeAsync(() =>
{
StatusText = update.StatusText;
StepsProgress = update.StepsProgress;
StepProgress = update.StepProgress;
}, DispatcherPriority.Background);
};
_logger.LogInformation("Installer created, starting the installation process"); _logger.LogInformation("Installer created, starting the installation process");
try try
{ {
var result = await _installer.Begin(CancellationToken.None); var result = await Task.Run(async () => await _installer.Begin(CancellationToken.None));
if (!result) throw new Exception("Installation failed"); if (!result) throw new Exception("Installation failed");
if (result) await SaveConfigAndContinue(_config); if (result) await SaveConfigAndContinue(_config);
@ -200,6 +208,7 @@ public class StandardInstallationViewModel : ViewModelBase
} }
} }
private async Task SaveConfigAndContinue(InstallerConfiguration config) private async Task SaveConfigAndContinue(InstallerConfiguration config)
{ {
var path = config.Install.Combine("modlist-image.png"); var path = config.Install.Combine("modlist-image.png");

View File

@ -27,11 +27,12 @@ public class InstallConfigurationViewModel : ViewModelBase, IActivatableViewMode
{ {
private readonly DTOSerializer _dtos; private readonly DTOSerializer _dtos;
private readonly InstallationStateManager _stateManager; private readonly InstallationStateManager _stateManager;
private readonly SettingsManager _settingsManager;
public InstallConfigurationViewModel(DTOSerializer dtos, InstallationStateManager stateManager, SettingsManager settingsManager)
public InstallConfigurationViewModel(DTOSerializer dtos, InstallationStateManager stateManager)
{ {
_stateManager = stateManager; _stateManager = stateManager;
_settingsManager = settingsManager;
_dtos = dtos; _dtos = dtos;
Activator = new ViewModelActivator(); Activator = new ViewModelActivator();
@ -51,22 +52,22 @@ public class InstallConfigurationViewModel : ViewModelBase, IActivatableViewMode
this.WhenAnyValue(t => t.ModListPath) this.WhenAnyValue(t => t.ModListPath)
.Where(t => t != default) .Where(t => t != default)
.SelectAsync(disposables, async x => await LoadModList(x)) .SelectMany(async x => await LoadModList(x))
.Select(x => x) .OnUIThread()
.ObserveOn(AvaloniaScheduler.Instance) .ObserveOn(AvaloniaScheduler.Instance)
.BindTo(this, t => t.ModList) .BindTo(this, t => t.ModList)
.DisposeWith(disposables); .DisposeWith(disposables);
this.WhenAnyValue(t => t.ModListPath) this.WhenAnyValue(t => t.ModListPath)
.Where(t => t != default) .Where(t => t != default)
.SelectAsync(disposables, async x => await LoadModListImage(x)) .SelectMany(async x => await LoadModListImage(x))
.ObserveOn(AvaloniaScheduler.Instance) .OnUIThread()
.BindTo(this, t => t.ModListImage) .BindTo(this, t => t.ModListImage)
.DisposeWith(disposables); .DisposeWith(disposables);
var settings = this.WhenAnyValue(t => t.ModListPath) var settings = this.WhenAnyValue(t => t.ModListPath)
.SelectAsync(disposables, async v => await _stateManager.Get(v)) .SelectMany(async v => await _stateManager.Get(v))
.ObserveOn(RxApp.MainThreadScheduler) .OnUIThread()
.Where(s => s != null); .Where(s => s != null);
settings.Select(s => s!.Install) settings.Select(s => s!.Install)
@ -76,9 +77,24 @@ public class InstallConfigurationViewModel : ViewModelBase, IActivatableViewMode
settings.Select(s => s!.Downloads) settings.Select(s => s!.Downloads)
.BindTo(this, vm => vm.Download) .BindTo(this, vm => vm.Download)
.DisposeWith(disposables); .DisposeWith(disposables);
LoadSettings().FireAndForget();
}); });
} }
private async Task LoadSettings()
{
var path = await _settingsManager.Load<AbsolutePath>("last-install-path");
if (path != default && path.FileExists())
{
Dispatcher.UIThread.Post(() => {
ModListPath = path;
});
}
}
[Reactive] public AbsolutePath ModListPath { get; set; } [Reactive] public AbsolutePath ModListPath { get; set; }
[Reactive] public AbsolutePath Install { get; set; } [Reactive] public AbsolutePath Install { get; set; }
@ -107,13 +123,15 @@ public class InstallConfigurationViewModel : ViewModelBase, IActivatableViewMode
if (metadataPath.FileExists()) if (metadataPath.FileExists())
metadata = _dtos.Deserialize<ModlistMetadata>(await metadataPath.ReadAllTextAsync()); metadata = _dtos.Deserialize<ModlistMetadata>(await metadataPath.ReadAllTextAsync());
_stateManager.SetLastState(new InstallationConfigurationSetting await _stateManager.SetLastState(new InstallationConfigurationSetting
{ {
ModList = ModListPath, ModList = ModListPath,
Downloads = Download, Downloads = Download,
Install = Install, Install = Install,
Metadata = metadata Metadata = metadata
}).FireAndForget(); });
await _settingsManager.Save("last-install-path", ModListPath);
MessageBus.Current.SendMessage(new NavigateTo(typeof(StandardInstallationViewModel))); MessageBus.Current.SendMessage(new NavigateTo(typeof(StandardInstallationViewModel)));
MessageBus.Current.SendMessage(new StartInstallation(ModListPath, Install, Download, metadata)); MessageBus.Current.SendMessage(new StartInstallation(ModListPath, Install, Download, metadata));

View File

@ -59,8 +59,8 @@ public class MainWindowViewModel : ReactiveValidationObject, IActivatableViewMod
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
BackButton = ReactiveCommand.Create(() => { Receive(new NavigateBack()); }, BackButton = ReactiveCommand.Create(() => { Receive(new NavigateBack()); },
this.ObservableForProperty(vm => vm.BreadCrumbs) this.WhenAnyValue(vm => vm.BreadCrumbs)
.Select(bc => bc.Value.Count() > 1)) .Select(bc => bc.Count() > 1))
.DisposeWith(disposables); .DisposeWith(disposables);
SettingsButton = ReactiveCommand.Create(() => { Receive(new NavigateTo(typeof(SettingsViewModel))); }) SettingsButton = ReactiveCommand.Create(() => { Receive(new NavigateTo(typeof(SettingsViewModel))); })
@ -68,6 +68,13 @@ public class MainWindowViewModel : ReactiveValidationObject, IActivatableViewMod
LogViewButton = ReactiveCommand.Create(() => { Receive(new NavigateTo(typeof(LogScreenViewModel))); }) LogViewButton = ReactiveCommand.Create(() => { Receive(new NavigateTo(typeof(LogScreenViewModel))); })
.DisposeWith(disposables); .DisposeWith(disposables);
this.WhenAnyValue(vm => vm.CurrentScreen)
.Where(view => view != default)
.Select(view => ((IScreenView) view).HumanName)
.Select(txt => txt == "" ? "Wabbajack" : $"Wabbajack - {txt}")
.BindTo(this, vm => vm.TitleText)
.DisposeWith(disposables);
}); });
CurrentScreen = (Control) _screens.First(s => s.ViewModelType == typeof(ModeSelectionViewModel)); CurrentScreen = (Control) _screens.First(s => s.ViewModelType == typeof(ModeSelectionViewModel));
@ -85,6 +92,8 @@ public class MainWindowViewModel : ReactiveValidationObject, IActivatableViewMod
[Reactive] public ReactiveCommand<Unit, Unit> LogViewButton { get; set; } [Reactive] public ReactiveCommand<Unit, Unit> LogViewButton { get; set; }
[Reactive] public string ResourceStatus { get; set; } [Reactive] public string ResourceStatus { get; set; }
[Reactive] public string TitleText { get; set; }
public ViewModelActivator Activator { get; } public ViewModelActivator Activator { get; }

View File

@ -8,7 +8,7 @@ namespace Wabbajack.App.Views;
public partial class GuidedWebView : ScreenBase<GuidedWebViewModel> public partial class GuidedWebView : ScreenBase<GuidedWebViewModel>
{ {
public GuidedWebView() : base(false) public GuidedWebView() : base("Web View", false)
{ {
InitializeComponent(); InitializeComponent();

View File

@ -9,39 +9,44 @@ using Wabbajack.App.ViewModels;
namespace Wabbajack.App.Views; namespace Wabbajack.App.Views;
public partial class InstallConfigurationView : ReactiveUserControl<InstallConfigurationViewModel>, IScreenView public partial class InstallConfigurationView : ScreenBase<InstallConfigurationViewModel>, IScreenView
{ {
public InstallConfigurationView() public InstallConfigurationView() : base("Install Configuration")
{ {
InitializeComponent(); InitializeComponent();
DataContext = App.Services.GetService<InstallConfigurationViewModel>()!; DataContext = App.Services.GetService<InstallConfigurationViewModel>()!;
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
this.Bind(ViewModel, x => x.ModListPath, ViewModel.WhenAnyValue(vm => vm.ModListPath)
view => view.ModListFile.SelectedPath) .Subscribe(path => ModListFile.Load(path))
.DisposeWith(disposables); .DisposeWith(disposables);
this.Bind(ViewModel, x => x.Download,
view => view.DownloadPath.SelectedPath) ViewModel.WhenAnyValue(vm => vm.ModListPath)
.Subscribe(path => ModListFile.Load(path))
.DisposeWith(disposables); .DisposeWith(disposables);
this.Bind(ViewModel, x => x.Install,
view => view.InstallPath.SelectedPath) ViewModel.WhenAnyValue(vm => vm.Download)
.Subscribe(path => DownloadPath.Load(path))
.DisposeWith(disposables);
ViewModel.WhenAnyValue(vm => vm.Install)
.Subscribe(path => InstallPath.Load(path))
.DisposeWith(disposables); .DisposeWith(disposables);
ViewModel.WhenAnyValue(x => x.BeginCommand) this.WhenAnyValue(view => view.ModListFile.SelectedPath)
.Where(x => x != default) .BindTo(ViewModel, vm => vm.ModListPath)
.BindTo(BeginInstall, x => x.Button.Command)
.DisposeWith(disposables); .DisposeWith(disposables);
ViewModel.WhenAnyValue(x => x.ModList) this.WhenAnyValue(view => view.DownloadPath.SelectedPath)
.Where(x => x != default) .BindTo(ViewModel, vm => vm.Download)
.Select(x => x!.Name)
.BindTo(ModListName, x => x.Text)
.DisposeWith(disposables); .DisposeWith(disposables);
ViewModel.WhenAnyValue(x => x.ModListImage) this.WhenAnyValue(view => view.InstallPath.SelectedPath)
.Where(x => x != default) .BindTo(ViewModel, vm => vm.Install)
.BindTo(ModListImage, x => x.Source) .DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.BeginCommand, view => view.BeginInstall.Button)
.DisposeWith(disposables); .DisposeWith(disposables);
}); });
} }

View File

@ -32,25 +32,26 @@
</Design.DataContext> </Design.DataContext>
<Grid RowDefinitions="40, *"> <Grid RowDefinitions="40, *">
<Grid ColumnDefinitions="40, *, 40, 40, 40, 40"> <Grid ColumnDefinitions="40, Auto, *, 40, 40, 40, 40">
<Button Grid.Column="0" x:Name="BackButton" x:FieldModifier="public"> <Button Grid.Column="0" x:Name="BackButton" x:FieldModifier="public">
<i:MaterialIcon Kind="ArrowBack" /> <i:MaterialIcon Kind="ArrowBack" />
</Button> </Button>
<TextBlock Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Center" x:Name="TitleText"></TextBlock>
<TextBlock Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" x:Name="ResourceStatus" /> <TextBlock Grid.Column="2" HorizontalAlignment="Right" VerticalAlignment="Center" x:Name="ResourceStatus" />
<Button Grid.Column="2" x:Name="LogButton"> <Button Grid.Column="3" x:Name="LogButton">
<i:MaterialIcon Kind="ViewList" /> <i:MaterialIcon Kind="ViewList" />
</Button> </Button>
<Button Grid.Column="3" x:Name="SettingsButton"> <Button Grid.Column="4" x:Name="SettingsButton">
<i:MaterialIcon Kind="Gear" /> <i:MaterialIcon Kind="Gear" />
</Button> </Button>
<Button Grid.Column="4" x:Name="MinimizeButton"> <Button Grid.Column="5" x:Name="MinimizeButton">
<i:MaterialIcon Kind="WindowMinimize" /> <i:MaterialIcon Kind="WindowMinimize" />
</Button> </Button>
<Button Grid.Column="5" x:Name="CloseButton" x:FieldModifier="public"> <Button Grid.Column="6" x:Name="CloseButton" x:FieldModifier="public">
<i:MaterialIcon Kind="Close" /> <i:MaterialIcon Kind="Close" />
</Button> </Button>

View File

@ -16,27 +16,30 @@ public partial class MainWindow : ReactiveWindow<MainWindowViewModel>
InitializeComponent(); InitializeComponent();
DataContext = App.Services.GetService<MainWindowViewModel>()!; DataContext = App.Services.GetService<MainWindowViewModel>()!;
this.WhenActivated(dispose => this.WhenActivated(disposables =>
{ {
CloseButton.Command = ReactiveCommand.Create(() => Environment.Exit(0)) CloseButton.Command = ReactiveCommand.Create(() => Environment.Exit(0))
.DisposeWith(dispose); .DisposeWith(disposables);
MinimizeButton.Command = ReactiveCommand.Create(() => WindowState = WindowState.Minimized) MinimizeButton.Command = ReactiveCommand.Create(() => WindowState = WindowState.Minimized)
.DisposeWith(dispose); .DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.BackButton, view => view.BackButton) this.BindCommand(ViewModel, vm => vm.BackButton, view => view.BackButton)
.DisposeWith(dispose); .DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SettingsButton, view => view.SettingsButton) this.BindCommand(ViewModel, vm => vm.SettingsButton, view => view.SettingsButton)
.DisposeWith(dispose); .DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.LogViewButton, view => view.LogButton) this.BindCommand(ViewModel, vm => vm.LogViewButton, view => view.LogButton)
.DisposeWith(dispose); .DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CurrentScreen, view => view.Contents.Content) this.OneWayBind(ViewModel, vm => vm.CurrentScreen, view => view.Contents.Content)
.DisposeWith(dispose); .DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.ResourceStatus, view => view.ResourceStatus.Text) this.OneWayBind(ViewModel, vm => vm.ResourceStatus, view => view.ResourceStatus.Text)
.DisposeWith(dispose); .DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.TitleText, view => view.TitleText.Text)
.DisposeWith(disposables);
}); });

View File

@ -9,7 +9,7 @@ namespace Wabbajack.App.Views;
public partial class ModeSelectionView : ScreenBase<ModeSelectionViewModel> public partial class ModeSelectionView : ScreenBase<ModeSelectionViewModel>
{ {
public ModeSelectionView(IServiceProvider provider) public ModeSelectionView(IServiceProvider provider) : base("")
{ {
InitializeComponent(); InitializeComponent();
this.WhenActivated(disposables => this.WhenActivated(disposables =>

View File

@ -1,4 +1,5 @@
using System; using System;
using ReactiveUI.Fody.Helpers;
using Wabbajack.App.Interfaces; using Wabbajack.App.Interfaces;
using Wabbajack.App.ViewModels; using Wabbajack.App.ViewModels;
@ -7,9 +8,13 @@ namespace Wabbajack.App.Views;
public abstract class ScreenBase<T> : ViewBase<T>, IScreenView public abstract class ScreenBase<T> : ViewBase<T>, IScreenView
where T : ViewModelBase where T : ViewModelBase
{ {
protected ScreenBase(bool createViewModel = true) : base(createViewModel) protected ScreenBase(string humanName, bool createViewModel = true) : base(createViewModel)
{ {
HumanName = humanName;
} }
public Type ViewModelType => typeof(T); public Type ViewModelType => typeof(T);
[Reactive]
public string HumanName { get; set; }
} }

View File

@ -67,7 +67,8 @@ public class FileExtractor
Predicate<RelativePath> shouldExtract, Predicate<RelativePath> shouldExtract,
Func<RelativePath, IExtractedFile, ValueTask<T>> mapfn, Func<RelativePath, IExtractedFile, ValueTask<T>> mapfn,
CancellationToken token, CancellationToken token,
HashSet<RelativePath>? onlyFiles = null) HashSet<RelativePath>? onlyFiles = null,
Action<Percent>? progressFunction = null)
{ {
if (sFn is NativeFileStreamFactory) _logger.LogInformation("Extracting {file}", sFn.Name); if (sFn is NativeFileStreamFactory) _logger.LogInformation("Extracting {file}", sFn.Name);
await using var archive = await sFn.GetStream(); await using var archive = await sFn.GetStream();
@ -92,7 +93,7 @@ public class FileExtractor
{ {
await using var tempFolder = _manager.CreateFolder(); await using var tempFolder = _manager.CreateFolder();
results = await GatheringExtractWith7Zip(sFn, shouldExtract, results = await GatheringExtractWith7Zip(sFn, shouldExtract,
mapfn, onlyFiles, token); mapfn, onlyFiles, token, progressFunction);
} }
break; break;
@ -179,7 +180,8 @@ public class FileExtractor
Predicate<RelativePath> shouldExtract, Predicate<RelativePath> shouldExtract,
Func<RelativePath, IExtractedFile, ValueTask<T>> mapfn, Func<RelativePath, IExtractedFile, ValueTask<T>> mapfn,
IReadOnlyCollection<RelativePath>? onlyFiles, IReadOnlyCollection<RelativePath>? onlyFiles,
CancellationToken token) CancellationToken token,
Action<Percent>? progressFunction = null)
{ {
TemporaryPath? tmpFile = null; TemporaryPath? tmpFile = null;
await using var dest = _manager.CreateFolder(); await using var dest = _manager.CreateFolder();
@ -262,6 +264,9 @@ public class FileExtractor
var newPosition = percentInt == 0 ? 0 : totalSize / percentInt; var newPosition = percentInt == 0 ? 0 : totalSize / percentInt;
var throughput = newPosition - oldPosition; var throughput = newPosition - oldPosition;
job.ReportNoWait((int) throughput); job.ReportNoWait((int) throughput);
progressFunction?.Invoke(Percent.FactoryPutInRange(lastPercent, 100));
lastPercent = percentInt; lastPercent = percentInt;
}, token); }, token);
@ -306,7 +311,7 @@ public class FileExtractor
} }
public async Task ExtractAll(AbsolutePath src, AbsolutePath dest, CancellationToken token, public async Task ExtractAll(AbsolutePath src, AbsolutePath dest, CancellationToken token,
Predicate<RelativePath>? filterFn = null) Predicate<RelativePath>? filterFn = null, Action<Percent>? updateProgress = null)
{ {
filterFn ??= _ => true; filterFn ??= _ => true;
await GatheringExtract(new NativeFileStreamFactory(src), filterFn, async (path, factory) => await GatheringExtract(new NativeFileStreamFactory(src), filterFn, async (path, factory) =>
@ -316,6 +321,6 @@ public class FileExtractor
await using var stream = await factory.GetStream(); await using var stream = await factory.GetStream();
await abs.WriteAllAsync(stream, token); await abs.WriteAllAsync(stream, token);
return 0; return 0;
}, token); }, token, progressFunction: updateProgress);
} }
} }

View File

@ -51,11 +51,11 @@ public abstract class AInstaller<T>
private long _currentStepProgress; private long _currentStepProgress;
private long _maxStepProgress; protected long MaxStepProgress { get; set; }
private string _statusText; private string _statusText;
private readonly Stopwatch _updateStopWatch = new(); private readonly Stopwatch _updateStopWatch = new();
public Func<StatusUpdate, Task>? OnStatusUpdate; public Action<StatusUpdate>? OnStatusUpdate;
public AInstaller(ILogger<T> logger, InstallerConfiguration config, IGameLocator gameLocator, public AInstaller(ILogger<T> logger, InstallerConfiguration config, IGameLocator gameLocator,
@ -87,41 +87,42 @@ public abstract class AInstaller<T>
public ModList ModList => _configuration.ModList; public ModList ModList => _configuration.ModList;
public async Task NextStep(string statusText, long maxStepProgress) public void NextStep(string statusText, long maxStepProgress)
{ {
_updateStopWatch.Restart(); _updateStopWatch.Restart();
_maxStepProgress = maxStepProgress; MaxStepProgress = maxStepProgress;
_currentStep += 1; _currentStep += 1;
_statusText = statusText; _statusText = statusText;
_logger.LogInformation("Next Step: {Step}", statusText); _logger.LogInformation("Next Step: {Step}", statusText);
if (OnStatusUpdate != null) OnStatusUpdate?.Invoke(new StatusUpdate($"[{_currentStep}/{MaxSteps}] " + statusText,
await OnStatusUpdate!(new StatusUpdate($"[{_currentStep}/{MaxSteps}] " + statusText, Percent.FactoryPutInRange(_currentStep, MaxSteps), Percent.Zero));
Percent.FactoryPutInRange(_currentStep, MaxSteps), Percent.Zero));
} }
public async ValueTask UpdateProgress(long stepProgress) public void UpdateProgress(long stepProgress)
{ {
Interlocked.Add(ref _currentStepProgress, stepProgress); Interlocked.Add(ref _currentStepProgress, stepProgress);
if (_updateStopWatch.ElapsedMilliseconds < _limitMS) return; OnStatusUpdate?.Invoke(new StatusUpdate($"[{_currentStep}/{MaxSteps}] " + _statusText, Percent.FactoryPutInRange(_currentStep, MaxSteps),
lock (_updateStopWatch) Percent.FactoryPutInRange(_currentStepProgress, MaxStepProgress)));
{
if (_updateStopWatch.ElapsedMilliseconds < _limitMS) return;
_updateStopWatch.Restart();
}
if (OnStatusUpdate != null)
await OnStatusUpdate!(new StatusUpdate(_statusText, Percent.FactoryPutInRange(_currentStep, MaxSteps),
Percent.FactoryPutInRange(_currentStepProgress, _maxStepProgress)));
} }
public abstract Task<bool> Begin(CancellationToken token); public abstract Task<bool> Begin(CancellationToken token);
public async Task ExtractModlist(CancellationToken token) protected async Task ExtractModlist(CancellationToken token)
{ {
ExtractedModlistFolder = _manager.CreateFolder(); ExtractedModlistFolder = _manager.CreateFolder();
await _extractor.ExtractAll(_configuration.ModlistArchive, ExtractedModlistFolder, token); await using var stream = _configuration.ModlistArchive.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
using var archive = new ZipArchive(stream, ZipArchiveMode.Read);
NextStep("Extracting Modlist", archive.Entries.Count);
foreach (var entry in archive.Entries)
{
var path = entry.FullName.ToRelativePath().RelativeTo(ExtractedModlistFolder);
path.Parent.CreateDirectory();
await using var of = path.Open(FileMode.Create, FileAccess.Write, FileShare.None);
await entry.Open().CopyToAsync(of, token);
UpdateProgress(1);
}
} }
public async Task<byte[]> LoadBytesFromPath(RelativePath path) public async Task<byte[]> LoadBytesFromPath(RelativePath path)
@ -165,13 +166,15 @@ public abstract class AInstaller<T>
/// </summary> /// </summary>
protected async Task PrimeVFS() protected async Task PrimeVFS()
{ {
NextStep("Priming VFS", 0);
_vfs.AddKnown(_configuration.ModList.Directives.OfType<FromArchive>().Select(d => d.ArchiveHashPath), _vfs.AddKnown(_configuration.ModList.Directives.OfType<FromArchive>().Select(d => d.ArchiveHashPath),
HashedArchives); HashedArchives);
await _vfs.BackfillMissing(); await _vfs.BackfillMissing();
} }
public void BuildFolderStructure() public async Task BuildFolderStructure()
{ {
NextStep("Building Folder Structure", 0);
_logger.LogInformation("Building Folder Structure"); _logger.LogInformation("Building Folder Structure");
ModList.Directives ModList.Directives
.Where(d => d.To.Depth > 1) .Where(d => d.To.Depth > 1)
@ -182,7 +185,7 @@ public abstract class AInstaller<T>
public async Task InstallArchives(CancellationToken token) public async Task InstallArchives(CancellationToken token)
{ {
await NextStep("Installing files", ModList.Directives.Sum(d => d.Size)); NextStep("Installing files", ModList.Directives.Sum(d => d.Size));
var grouped = ModList.Directives var grouped = ModList.Directives
.OfType<FromArchive>() .OfType<FromArchive>()
.Select(a => new {VF = _vfs.Index.FileForArchiveHashPath(a.ArchiveHashPath), Directive = a}) .Select(a => new {VF = _vfs.Index.FileForArchiveHashPath(a.ArchiveHashPath), Directive = a})
@ -196,7 +199,7 @@ public abstract class AInstaller<T>
foreach (var directive in grouped[vf]) foreach (var directive in grouped[vf])
{ {
var file = directive.Directive; var file = directive.Directive;
await UpdateProgress(file.Size); UpdateProgress(file.Size);
switch (file) switch (file)
{ {
@ -284,7 +287,7 @@ public abstract class AInstaller<T>
} }
_logger.LogInformation("Downloading {count} archives", missing.Count); _logger.LogInformation("Downloading {count} archives", missing.Count);
await NextStep("Downloading files", missing.Count); NextStep("Downloading files", missing.Count);
await missing await missing
.OrderBy(a => a.Size) .OrderBy(a => a.Size)
@ -305,7 +308,7 @@ public abstract class AInstaller<T>
} }
await DownloadArchive(archive, download, token, outputPath); await DownloadArchive(archive, download, token, outputPath);
await UpdateProgress(1); UpdateProgress(1);
}); });
} }
@ -345,23 +348,30 @@ public abstract class AInstaller<T>
public async Task HashArchives(CancellationToken token) public async Task HashArchives(CancellationToken token)
{ {
NextStep("Hashing Archives", 0);
_logger.LogInformation("Looking for files to hash"); _logger.LogInformation("Looking for files to hash");
var allFiles = _configuration.Downloads.EnumerateFiles() var allFiles = _configuration.Downloads.EnumerateFiles()
.Concat(_gameLocator.GameLocation(_configuration.Game).EnumerateFiles()) .Concat(_gameLocator.GameLocation(_configuration.Game).EnumerateFiles())
.ToList(); .ToList();
var hashDict = allFiles.GroupBy(f => f.Size()).ToDictionary(g => g.Key); var hashDict = allFiles.GroupBy(f => f.Size()).ToDictionary(g => g.Key);
var toHash = ModList.Archives.Where(a => hashDict.ContainsKey(a.Size)) var toHash = ModList.Archives.Where(a => hashDict.ContainsKey(a.Size))
.SelectMany(a => hashDict[a.Size]).ToList(); .SelectMany(a => hashDict[a.Size]).ToList();
MaxStepProgress = toHash.Count;
_logger.LogInformation("Found {count} total files, {hashedCount} matching filesize", allFiles.Count, _logger.LogInformation("Found {count} total files, {hashedCount} matching filesize", allFiles.Count,
toHash.Count); toHash.Count);
var hashResults = await var hashResults = await
toHash toHash
.PMapAll(async e => (await _fileHashCache.FileHashCachedAsync(e, token), e)) .PMapAll(async e =>
{
UpdateProgress(1);
return (await _fileHashCache.FileHashCachedAsync(e, token), e);
})
.ToList(); .ToList();
HashedArchives = hashResults HashedArchives = hashResults
@ -389,11 +399,11 @@ public abstract class AInstaller<T>
var savePath = (RelativePath) "saves"; var savePath = (RelativePath) "saves";
var existingFiles = _configuration.Install.EnumerateFiles().ToList(); var existingFiles = _configuration.Install.EnumerateFiles().ToList();
await NextStep("Optimizing Modlist: Looking for files to delete", existingFiles.Count); NextStep("Optimizing Modlist: Looking for files to delete", existingFiles.Count);
await existingFiles await existingFiles
.PDoAll(async f => .PDoAll(async f =>
{ {
await UpdateProgress(1); UpdateProgress(1);
var relativeTo = f.RelativeTo(_configuration.Install); var relativeTo = f.RelativeTo(_configuration.Install);
if (indexed.ContainsKey(relativeTo) || f.InFolder(_configuration.Downloads)) if (indexed.ContainsKey(relativeTo) || f.InFolder(_configuration.Downloads))
return; return;
@ -408,12 +418,12 @@ public abstract class AInstaller<T>
}); });
_logger.LogInformation("Cleaning empty folders"); _logger.LogInformation("Cleaning empty folders");
await NextStep("Optimizing Modlist: Cleaning empty folders", indexed.Keys.Count); NextStep("Optimizing Modlist: Cleaning empty folders", indexed.Keys.Count);
var expectedFolders = (await indexed.Keys var expectedFolders = (indexed.Keys
.Select(f => f.RelativeTo(_configuration.Install)) .Select(f => f.RelativeTo(_configuration.Install))
// We ignore the last part of the path, so we need a dummy file name // We ignore the last part of the path, so we need a dummy file name
.Append(_configuration.Downloads.Combine("_")) .Append(_configuration.Downloads.Combine("_"))
.OnEach(async _ => await UpdateProgress(1)) .OnEach(_ => UpdateProgress(1))
.Where(f => f.InFolder(_configuration.Install)) .Where(f => f.InFolder(_configuration.Install))
.SelectMany(path => .SelectMany(path =>
{ {
@ -443,10 +453,9 @@ public abstract class AInstaller<T>
var existingfiles = _configuration.Install.EnumerateFiles().ToHashSet(); var existingfiles = _configuration.Install.EnumerateFiles().ToHashSet();
await NextStep("Optimizing Modlist: Removing redundant directives", indexed.Count); NextStep("Optimizing Modlist: Removing redundant directives", indexed.Count);
await indexed.Values.PMapAll<Directive, Directive?>(async d => await indexed.Values.PMapAll<Directive, Directive?>(async d =>
{ {
await UpdateProgress(1);
// Bit backwards, but we want to return null for // Bit backwards, but we want to return null for
// all files we *want* installed. We return the files // all files we *want* installed. We return the files
// to remove from the install list. // to remove from the install list.
@ -457,11 +466,13 @@ public abstract class AInstaller<T>
}) })
.Do(d => .Do(d =>
{ {
UpdateProgress(1);
if (d != null) indexed.Remove(d.To); if (d != null) indexed.Remove(d.To);
}); });
_logger.LogInformation("Optimized {optimized} directives to {indexed} required", ModList.Directives.Length, _logger.LogInformation("Optimized {optimized} directives to {indexed} required", ModList.Directives.Length,
indexed.Count); indexed.Count);
NextStep("Finalizing modlist optimization", 0);
var requiredArchives = indexed.Values.OfType<FromArchive>() var requiredArchives = indexed.Values.OfType<FromArchive>()
.GroupBy(d => d.ArchiveHashPath.Hash) .GroupBy(d => d.ArchiveHashPath.Hash)
.Select(d => d.Key) .Select(d => d.Key)

View File

@ -38,13 +38,14 @@ public class StandardInstaller : AInstaller<StandardInstaller>
base(logger, config, gameLocator, extractor, jsonSerializer, vfs, fileHashCache, downloadDispatcher, base(logger, config, gameLocator, extractor, jsonSerializer, vfs, fileHashCache, downloadDispatcher,
parallelOptions, wjClient) parallelOptions, wjClient)
{ {
MaxSteps = 7; MaxSteps = 14;
} }
public override async Task<bool> Begin(CancellationToken token) public override async Task<bool> Begin(CancellationToken token)
{ {
if (token.IsCancellationRequested) return false; if (token.IsCancellationRequested) return false;
await _wjClient.SendMetric(MetricNames.BeginInstall, ModList.Name); await _wjClient.SendMetric(MetricNames.BeginInstall, ModList.Name);
NextStep("Configuring Installer", 0);
_logger.LogInformation("Configuring Processor"); _logger.LogInformation("Configuring Processor");
if (_configuration.GameFolder == default) if (_configuration.GameFolder == default)
@ -84,8 +85,7 @@ public class StandardInstaller : AInstaller<StandardInstaller>
_configuration.Downloads.CreateDirectory(); _configuration.Downloads.CreateDirectory();
await OptimizeModlist(token); await OptimizeModlist(token);
await HashArchives(token); await HashArchives(token);
await DownloadArchives(token); await DownloadArchives(token);
@ -106,7 +106,7 @@ public class StandardInstaller : AInstaller<StandardInstaller>
await PrimeVFS(); await PrimeVFS();
BuildFolderStructure(); await BuildFolderStructure();
await InstallArchives(token); await InstallArchives(token);
@ -129,7 +129,7 @@ public class StandardInstaller : AInstaller<StandardInstaller>
await ExtractedModlistFolder!.DisposeAsync(); await ExtractedModlistFolder!.DisposeAsync();
await _wjClient.SendMetric(MetricNames.FinishInstall, ModList.Name); await _wjClient.SendMetric(MetricNames.FinishInstall, ModList.Name);
await NextStep("Finished", 1); NextStep("Finished", 1);
_logger.LogInformation("Finished Installation"); _logger.LogInformation("Finished Installation");
return true; return true;
} }
@ -259,12 +259,12 @@ public class StandardInstaller : AInstaller<StandardInstaller>
private async Task InstallIncludedFiles(CancellationToken token) private async Task InstallIncludedFiles(CancellationToken token)
{ {
_logger.LogInformation("Writing inline files"); _logger.LogInformation("Writing inline files");
await NextStep("Installing Included Files", ModList.Directives.OfType<InlineFile>().Count()); NextStep("Installing Included Files", ModList.Directives.OfType<InlineFile>().Count());
await ModList.Directives await ModList.Directives
.OfType<InlineFile>() .OfType<InlineFile>()
.PDoAll(async directive => .PDoAll(async directive =>
{ {
await UpdateProgress(1); UpdateProgress(1);
var outPath = _configuration.Install.Combine(directive.To); var outPath = _configuration.Install.Combine(directive.To);
outPath.Delete(); outPath.Delete();

View File

@ -30,7 +30,7 @@ public readonly struct Percent : IComparable, IEquatable<Percent>
public static bool InRange(double d) public static bool InRange(double d)
{ {
return d >= 0 || d <= 1; return d is >= 0 or <= 1;
} }
public static Percent operator +(Percent c1, Percent c2) public static Percent operator +(Percent c1, Percent c2)

View File

@ -172,7 +172,7 @@ public class Context
_knownArchives.TryAdd(key, value); _knownArchives.TryAdd(key, value);
} }
public async Task BackfillMissing() public async ValueTask BackfillMissing()
{ {
var newFiles = _knownArchives.ToDictionary(kv => kv.Key, var newFiles = _knownArchives.ToDictionary(kv => kv.Key,
kv => new VirtualFile kv => new VirtualFile