Compiler views and logger view controls

This commit is contained in:
Timothy Baldridge 2021-10-11 21:49:01 -06:00
parent 6a0ff5f29d
commit 6e52318dbb
20 changed files with 500 additions and 11 deletions

View File

@ -0,0 +1,17 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:Wabbajack.App.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Wabbajack.App.Controls.LogView">
<Border>
<ListBox x:Name="Messages">
<ListBox.ItemTemplate>
<DataTemplate>
<controls:LogViewItem></controls:LogViewItem>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</UserControl>

View File

@ -0,0 +1,22 @@
using Avalonia.Controls.Mixins;
using Avalonia.ReactiveUI;
using Microsoft.Extensions.DependencyInjection;
using ReactiveUI;
using Wabbajack.App.Utilities;
namespace Wabbajack.App.Controls;
public partial class LogView : ReactiveUserControl<LogViewModel>
{
public LogView()
{
DataContext = App.Services.GetService<LogViewModel>()!;
InitializeComponent();
this.WhenActivated(disposables =>
{
this.OneWayBind(ViewModel, vm => vm.Messages, view => view.Messages.Items)
.DisposeWith(disposables);
});
}
}

View File

@ -0,0 +1,8 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Wabbajack.App.Controls.LogViewItem">
<TextBlock x:Name="Message"></TextBlock>
</UserControl>

View File

@ -0,0 +1,20 @@
using Avalonia.Controls.Mixins;
using Avalonia.ReactiveUI;
using ReactiveUI;
using Wabbajack.App.Utilities;
namespace Wabbajack.App.Controls;
public partial class LogViewItem : ReactiveUserControl<LoggerProvider.ILogMessage>, IActivatableView
{
public LogViewItem()
{
InitializeComponent();
this.WhenActivated(disposables =>
{
this.OneWayBind(ViewModel, vm => vm.ShortMessage, view => view.Message.Text)
.DisposeWith(disposables);
});
}
}

View File

@ -0,0 +1,43 @@
using System;
using System.Collections.ObjectModel;
using Avalonia.Controls.Mixins;
using DynamicData;
using Microsoft.Extensions.Logging;
using ReactiveUI;
using Wabbajack.App.Utilities;
using Wabbajack.App.ViewModels;
namespace Wabbajack.App.Controls;
public class LogViewModel : ViewModelBase, IActivatableViewModel
{
private readonly LoggerProvider _provider;
private readonly SourceCache<LoggerProvider.ILogMessage, long> _messages;
public readonly ReadOnlyObservableCollection<LoggerProvider.ILogMessage> _messagesFiltered;
public ReadOnlyObservableCollection<LoggerProvider.ILogMessage> Messages => _messagesFiltered;
public LogViewModel(LoggerProvider provider)
{
_messages = new SourceCache<LoggerProvider.ILogMessage, long>(m => m.MessageId);
_messages.LimitSizeTo(100);
Activator = new ViewModelActivator();
_provider = provider;
_messages.Connect()
.Bind(out _messagesFiltered)
.Subscribe();
this.WhenActivated(disposables =>
{
_provider.Messages
.Subscribe(m => _messages.AddOrUpdate(m))
.DisposeWith(disposables);
});
}
}

View File

@ -0,0 +1,8 @@
using Wabbajack.Compiler;
namespace Wabbajack.App.Messages;
public record StartCompilation(CompilerSettings Settings)
{
}

View File

@ -45,8 +45,7 @@ namespace Wabbajack.App.Screens
private readonly IResource<DownloadDispatcher> _dispatcherLimiter;
private SourceCache<BrowseItemViewModel, string> _modLists = new(x => x.MachineURL);
public readonly ReadOnlyObservableCollection<BrowseItemViewModel> _filteredModLists;
public ReadOnlyObservableCollection<BrowseItemViewModel> ModLists => _filteredModLists;

View File

@ -0,0 +1,15 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:i="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:controls="clr-namespace:Wabbajack.App.Controls"
x:Class="Wabbajack.App.Screens.CompilationView">
<Grid RowDefinitions="40, 5, 5, *, 40">
<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>
<ProgressBar Grid.Row="2" x:Name="StepProgress" Maximum="10000" Value="30"></ProgressBar>
<controls:LogView Grid.Row="3" x:Name="LogView"></controls:LogView>
</Grid>
</UserControl>

View File

@ -0,0 +1,12 @@
using Wabbajack.App.ViewModels;
using Wabbajack.App.Views;
namespace Wabbajack.App.Screens;
public partial class CompilationView : ScreenBase<CompilationViewModel>
{
public CompilationView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,35 @@
using System;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;
using ReactiveUI;
using Wabbajack.App.Messages;
using Wabbajack.App.ViewModels;
using Wabbajack.Compiler;
namespace Wabbajack.App.Screens;
public class CompilationViewModel : ViewModelBase, IReceiverMarker, IReceiver<StartCompilation>
{
private readonly IServiceProvider _provider;
private ACompiler _compiler;
public CompilationViewModel(IServiceProvider provider)
{
_provider = provider;
Activator = new ViewModelActivator();
}
public void Receive(StartCompilation val)
{
if (val.Settings is MO2CompilerSettings mo2)
{
var compiler = _provider.GetService<MO2Compiler>()!;
compiler.Settings = mo2;
_compiler = compiler;
}
_compiler.Begin(CancellationToken.None);
}
}

View File

@ -4,8 +4,34 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:i="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:controls="clr-namespace:Wabbajack.App.Controls"
x:Class="Wabbajack.App.Screens.CompilerConfigurationView">
<Grid RowDefinitions="40, 5, 5, *, 40">
<Grid RowDefinitions="40, *, 40">
<TextBlock Grid.Row="0" x:Name="StatusText" FontSize="20" FontWeight="Bold">Compiler Configuration</TextBlock>
<Grid Grid.Row="1" ColumnDefinitions="Auto, *" RowDefinitions="Auto, Auto, Auto, Auto, Auto" Margin="4">
<Label Grid.Column="0" Grid.Row="0" HorizontalAlignment="Right">Title:</Label>
<TextBox Grid.Column="1" Grid.Row="0" x:Name="Title"></TextBox>
<Label Grid.Column="0" Grid.Row="1" HorizontalAlignment="Right">Settings File:</Label>
<controls:FileSelectionBox Grid.Column="1" Grid.Row="1" x:Name="SettingsFile"
AllowedExtensions=".txt|.json">
</controls:FileSelectionBox>
<Label Grid.Column="0" Grid.Row="2" HorizontalAlignment="Right">Mod List Folder:</Label>
<controls:FileSelectionBox Grid.Column="1" Grid.Row="2" x:Name="BaseFolder" SelectFolder="True"></controls:FileSelectionBox>
<Label Grid.Column="0" Grid.Row="3" HorizontalAlignment="Right">Downloads Folder:</Label>
<controls:FileSelectionBox Grid.Column="1" Grid.Row="3" x:Name="DownloadsFolder" SelectFolder="True"></controls:FileSelectionBox>
<Label Grid.Column="0" Grid.Row="4" HorizontalAlignment="Right">Base Game:</Label>
<ComboBox Grid.Column="1" Grid.Row="4" x:Name="BaseGame">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=HumanFriendlyGameName}"></TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
<Grid ColumnDefinitions="*, Auto" Grid.Row="2">
<Button Grid.Column="1" x:Name="StartCompilation">
<TextBlock>Start Compilation</TextBlock>
</Button>
</Grid>
</Grid>
</UserControl>

View File

@ -1,5 +1,8 @@
using System.Reactive.Disposables;
using Avalonia;
using ReactiveUI;
using Wabbajack.App.Views;
namespace Wabbajack.App.Screens
@ -9,6 +12,29 @@ namespace Wabbajack.App.Screens
public CompilerConfigurationView()
{
InitializeComponent();
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, vm => vm.BasePath, view => view.BaseFolder.SelectedPath)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SettingsFile, view => view.SettingsFile.SelectedPath)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Downloads, view => view.DownloadsFolder.SelectedPath)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.AllGames, view => view.BaseGame.Items)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.BaseGame, view => view.BaseGame.SelectedItem)
.DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.StartCompilation, view => view.StartCompilation)
.DisposeWith(disposables);
});
}
}
}

View File

@ -1,14 +1,128 @@
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Controls.Mixins;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.App.Extensions;
using Wabbajack.App.Messages;
using Wabbajack.App.ViewModels;
using Wabbajack.Compiler;
using Wabbajack.DTOs;
using Wabbajack.Installer;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
using Consts = Wabbajack.Compiler.Consts;
namespace Wabbajack.App.Screens;
public class CompilerConfigurationViewModel : ViewModelBase, IReceiverMarker
{
[Reactive]
public AbsolutePath SettingsFile { get; set; }
[Reactive]
public AbsolutePath Downloads { get; set; }
[Reactive]
public GameMetaData BaseGame { get; set; }
[Reactive]
public AbsolutePath BasePath { get; set; }
[Reactive]
public AbsolutePath GamePath { get; set; }
[Reactive]
public string SelectedProfile { get; set; }
[Reactive]
public IEnumerable<GameMetaData> AllGames { get; set; }
[Reactive]
public ReactiveCommand<Unit, Unit> StartCompilation { get; set; }
public CompilerConfigurationViewModel()
{
Activator = new ViewModelActivator();
AllGames = GameRegistry.Games.Values.ToArray();
StartCompilation = ReactiveCommand.Create(() => BeginCompilation());
this.WhenActivated(disposables =>
{
var tuples = this.WhenAnyValue(vm => vm.SettingsFile)
.Where(file => file != default)
.SelectAsync(disposables, InterpretSettingsFile)
.Where(t => t != default)
.ObserveOn(RxApp.MainThreadScheduler);
tuples.Select(t => t.Downloads)
.BindTo(this, vm => vm.Downloads)
.DisposeWith(disposables);
tuples.Select(t => t.Root)
.BindTo(this, vm => vm.BasePath)
.DisposeWith(disposables);
tuples.Select(t => t.Game)
.BindTo(this, vm => vm.BaseGame)
.DisposeWith(disposables);
tuples.Select(t => t.SelectedProfile)
.BindTo(this, vm => vm.SelectedProfile)
.DisposeWith(disposables);
});
}
private void BeginCompilation()
{
var settings = new MO2CompilerSettings
{
Downloads = Downloads,
Source = BasePath,
Game = BaseGame.Game,
Profile = SelectedProfile
};
MessageBus.Instance.Send(new StartCompilation(settings));
MessageBus.Instance.Send(new NavigateTo(typeof(CompilationViewModel)));
}
public async ValueTask<(AbsolutePath Root, AbsolutePath Downloads, AbsolutePath Settings, GameMetaData Game, string SelectedProfile)>
InterpretSettingsFile(AbsolutePath settingsFile)
{
if (settingsFile.FileName == "modlist.txt".ToRelativePath() && settingsFile.Depth > 3)
{
var mo2Folder = settingsFile.Parent.Parent.Parent;
var compilerSettingsFile = settingsFile.Parent.Combine(Consts.CompilerSettings);
var mo2Ini = mo2Folder.Combine(Consts.MO2IniName);
if (mo2Ini.FileExists())
{
var iniData = mo2Ini.LoadIniFile();
var general = iniData["General"];
var game = GameRegistry.GetByFuzzyName(general["gameName"].FromMO2Ini());
var selectedProfile = general["selected_profile"].FromMO2Ini();
var gamePath = general["gamePath"].FromMO2Ini().ToAbsolutePath();
var settings = iniData["Settings"];
var downloadFolder = settings["download_directory"].FromMO2Ini().ToAbsolutePath();
return (mo2Folder, downloadFolder, compilerSettingsFile, game, selectedProfile);
}
}
return default;
}
}

View File

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Avalonia.Threading;
using CefNet;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Wabbajack.App.Controls;
using Wabbajack.App.Interfaces;
using Wabbajack.App.Messages;
@ -31,12 +32,15 @@ namespace Wabbajack.App
{
public static IServiceCollection AddAppServices(this IServiceCollection services)
{
services.AddAllSingleton<ILoggerProvider, LoggerProvider>();
services.AddSingleton<MessageBus>();
services.AddSingleton<MainWindow>();
services.AddSingleton<BrowseViewModel>();
services.AddTransient<BrowseItemViewModel>();
services.AddTransient<LogViewModel>();
services.AddDTOConverters();
services.AddDTOSerializer();
services.AddSingleton<ModeSelectionViewModel>();
@ -45,6 +49,7 @@ namespace Wabbajack.App
services.AddSingleton<IScreenView, InstallConfigurationView>();
services.AddSingleton<IScreenView, CompilerConfigurationView>();
services.AddSingleton<IScreenView, StandardInstallationView>();
services.AddSingleton<IScreenView, CompilationView>();
services.AddSingleton<IScreenView, SettingsView>();
services.AddSingleton<IScreenView, BrowseView>();
services.AddSingleton<IScreenView, LauncherView>();
@ -60,6 +65,7 @@ namespace Wabbajack.App
services.AddAllSingleton<IReceiverMarker, NexusLoginViewModel>();
services.AddAllSingleton<IReceiverMarker, LoversLabOAuthLoginViewModel>();
services.AddAllSingleton<IReceiverMarker, VectorPlexusOAuthLoginViewModel>();
services.AddAllSingleton<IReceiverMarker, CompilationViewModel>();
services.AddAllSingleton<IReceiverMarker, LauncherViewModel>();
// Services

View File

@ -0,0 +1,72 @@
using System;
using System.Collections.Immutable;
using System.Reactive.Disposables;
using System.Reactive.Subjects;
using System.Threading;
using Microsoft.Extensions.Logging;
namespace Wabbajack.App.Utilities;
public class LoggerProvider : ILoggerProvider
{
private Subject<ILogMessage> _messages = new();
public IObservable<ILogMessage> Messages => _messages;
private long _messageID = 0;
public long NextMessageId()
{
return Interlocked.Increment(ref _messageID);
}
public void Dispose()
{
_messages.Dispose();
}
public ILogger CreateLogger(string categoryName)
{
return new Logger(this, categoryName);
}
public class Logger : ILogger
{
private readonly LoggerProvider _provider;
private ImmutableList<object> Scopes = ImmutableList<object>.Empty;
private readonly string _categoryName;
public Logger(LoggerProvider provider, string categoryName)
{
_categoryName = categoryName;
_provider = provider;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
_provider._messages.OnNext(new LogMessage<TState>(_provider.NextMessageId(), logLevel, eventId, state, exception, formatter));
}
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
public IDisposable BeginScope<TState>(TState state)
{
Scopes = Scopes.Add(state);
return Disposable.Create(() => Scopes = Scopes.Remove(state));
}
}
public interface ILogMessage
{
long MessageId { get; }
string ShortMessage { get; }
}
record LogMessage<TState>(long MessageId, LogLevel LogLevel, EventId EventId, TState State, Exception? Exception, Func<TState, Exception?, string> Formatter) : ILogMessage
{
public string ShortMessage => Formatter(State, Exception);
}
}

View File

@ -37,7 +37,7 @@ namespace Wabbajack.Compiler
private readonly FileHashCache _hashCache;
protected readonly Context _vfs;
private readonly TemporaryFileManager _manager;
public readonly CompilerSettings _settings;
public CompilerSettings _settings;
private readonly AbsolutePath _stagingFolder;
public readonly ParallelOptions _parallelOptions;
@ -72,7 +72,13 @@ namespace Wabbajack.Compiler
}
public CompilerSettings Settings { get; set; }
public abstract Task<bool> Begin(CancellationToken token);
public CompilerSettings Settings
{
get => _settings;
set => _settings = value;
}
public Dictionary<Game, HashSet<Hash>> GameHashes { get; set; } = new Dictionary<Game, HashSet<Hash>>();
public Dictionary<Hash, Game[]> GamesWithHashes { get; set; } = new Dictionary<Hash, Game[]>();

View File

@ -50,7 +50,7 @@ namespace Wabbajack.Compiler
public static string DOWNLOAD_PATH_MAGIC_BACK = "{--||DOWNLOAD_PATH_MAGIC_BACK||--}";
public static string DOWNLOAD_PATH_MAGIC_DOUBLE_BACK = "{--||DOWNLOAD_PATH_MAGIC_DOUBLE_BACK||--}";
public static string DOWNLOAD_PATH_MAGIC_FORWARD = "{--||DOWNLOAD_PATH_MAGIC_FORWARD||--}";
public static RelativePath MO2IniName => "ModOrganizer.ini".ToRelativePath();
public static object CompilerSettings => "compiler_settings.json".ToRelativePath();
}
}

View File

@ -21,14 +21,13 @@ namespace Wabbajack.Compiler
{
public class MO2Compiler : ACompiler
{
private readonly MO2CompilerSettings _mo2Settings;
private MO2CompilerSettings _mo2Settings => (MO2CompilerSettings)Settings;
public MO2Compiler(ILogger<MO2Compiler> logger, FileExtractor.FileExtractor extractor, FileHashCache hashCache, Context vfs,
TemporaryFileManager manager, MO2CompilerSettings settings, ParallelOptions parallelOptions, DownloadDispatcher dispatcher,
Client wjClient, IGameLocator locator, DTOSerializer dtos, IBinaryPatchCache patchCache) :
base(logger, extractor, hashCache, vfs, manager, settings, parallelOptions, dispatcher, wjClient, locator, dtos, patchCache)
{
_mo2Settings = settings;
}
public AbsolutePath MO2ModsFolder => _settings.Source.Combine(Consts.MO2ModFolderName);
@ -45,7 +44,7 @@ namespace Wabbajack.Compiler
return mo2Folder.Combine("downloads");
}
public async Task<bool> Begin(CancellationToken token)
public override async Task<bool> Begin(CancellationToken token)
{
await _wjClient.SendMetric("begin_compiling", _mo2Settings.Profile);

View File

@ -1,6 +1,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using IniParser;
using IniParser.Exceptions;
using IniParser.Model;
using IniParser.Model.Configuration;
using IniParser.Parser;
@ -39,5 +43,60 @@ namespace Wabbajack.Installer
return new FileIniDataParser(IniParser()).ReadData(
new StreamReader(new MemoryStream(Encoding.UTF8.GetBytes(file))));
}
public static string FromMO2Ini(this string s)
{
if (string.IsNullOrEmpty(s))
{
return s;
}
if (s.StartsWith("@ByteArray(") && s.EndsWith(")"))
{
return UnescapeUTF8(s.Substring("@ByteArray(".Length, s.Length - "@ByteArray(".Length - ")".Length));
}
return UnescapeString(s);
}
private static string UnescapeString(string s)
{
if (s.Trim().StartsWith("\"") || s.Contains("\\\\"))
return Regex.Unescape(s.Trim('"'));
return s;
}
private static string UnescapeUTF8(string s)
{
var acc = new List<byte>();
for (var i = 0; i < s.Length; i++)
{
var c = s[i];
switch (c)
{
case '\\':
i++;
var nc = s[i];
switch (nc)
{
case '\\':
acc.Add((byte)'\\');
break;
case 'x':
var chrs = s[i + 1] + s[i + 2].ToString();
i += 2;
acc.Add(Convert.ToByte(chrs, 16));
break;
default:
throw new ParsingException($"Not a valid escape characer {nc}");
}
break;
default:
acc.Add((byte)c);
break;
}
}
return Encoding.UTF8.GetString(acc.ToArray());
}
}
}

View File

@ -66,6 +66,8 @@ namespace Wabbajack.Paths
}
}
public int Depth => Parts.Length;
public AbsolutePath ReplaceExtension(Extension newExtension)
{
var paths = new string[Parts.Length];