Settings manager

This commit is contained in:
Timothy Baldridge 2021-10-13 22:46:43 -06:00
parent f4f4b80968
commit 51015eaa81
9 changed files with 257 additions and 96 deletions

View File

@ -9,6 +9,6 @@
<Button x:Name="DeleteButton">
<i:MaterialIcon Kind="MinusCircle"></i:MaterialIcon>
</Button>
<TextBlock x:Name="Text"></TextBlock>
<TextBlock x:Name="Text" VerticalAlignment="Center"></TextBlock>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,62 @@
using System;
using System.IO;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using Wabbajack.Common;
using Wabbajack.DTOs.JsonConverters;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
namespace Wabbajack.App.Models;
public class SettingsManager
{
private readonly Configuration _configuration;
private readonly DTOSerializer _dtos;
private readonly ILogger<SettingsManager> _logger;
public SettingsManager(ILogger<SettingsManager> logger, Configuration configuration, DTOSerializer dtos)
{
_logger = logger;
_dtos = dtos;
_configuration = configuration;
_configuration.SavedSettingsLocation.CreateDirectory();
}
private AbsolutePath GetPath(string key) => _configuration.SavedSettingsLocation.Combine(key).WithExtension(Ext.Json);
public async Task Save<T>(string key, T value)
{
var tmp = GetPath(key).WithExtension(Ext.Temp);
await using (var s = tmp.Open(FileMode.Create, FileAccess.Write))
{
await JsonSerializer.SerializeAsync(s, value, _dtos.Options);
}
await tmp.MoveToAsync(GetPath(key), true, CancellationToken.None);
}
public async Task<T> Load<T>(string key)
where T : new()
{
var path = GetPath(key);
try
{
if (path.FileExists())
{
await using (var s = path.Open(FileMode.Create, FileAccess.Write))
{
await JsonSerializer.DeserializeAsync<T>(s, _dtos.Options);
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Loading settings {Key}", key);
}
return new T();
}
}

View File

@ -15,8 +15,8 @@
<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="2" HorizontalAlignment="Right">Source:</Label>
<controls:FileSelectionBox Grid.Column="1" Grid.Row="2" x:Name="Source" 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>
@ -51,8 +51,11 @@
</Grid>
<Grid ColumnDefinitions="*, Auto" Grid.Row="2">
<Button Grid.Column="1" x:Name="StartCompilation">
<Grid ColumnDefinitions="*, Auto, Auto" Grid.Row="2">
<Button Grid.Column="1" x:Name="InferSettings" Click="InferSettings_OnClick">
<TextBlock>Infer Settings</TextBlock>
</Button>
<Button Grid.Column="2" x:Name="StartCompilation">
<TextBlock>Start Compilation</TextBlock>
</Button>
</Grid>

View File

@ -1,8 +1,11 @@
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Threading;
using ReactiveUI;
using Wabbajack.App.Controls;
using Wabbajack.App.Views;
@ -20,11 +23,17 @@ namespace Wabbajack.App.Screens
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.Title, view => view.Title.Text)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SettingsFile, view => view.SettingsFile.SelectedPath)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Source, view => view.Source.SelectedPath)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Downloads, view => view.DownloadsFolder.SelectedPath)
.DisposeWith(disposables);
@ -61,5 +70,21 @@ namespace Wabbajack.App.Screens
if (!string.IsNullOrWhiteSpace(result))
ViewModel!.AddAlwaysExcluded(result.ToAbsolutePath());
}
private void InferSettings_OnClick(object? sender, RoutedEventArgs e)
{
Dispatcher.UIThread.InvokeAsync(async () =>
{
var dialog = new OpenFileDialog()
{
Title = "Select a modlist.txt file",
Filters = new List<FileDialogFilter> { new() {Extensions = new List<string> {"txt"}, Name = "modlist.txt"}},
AllowMultiple = false
};
var result = await dialog.ShowAsync(App.MainWindow);
if (result is { Length: > 0 })
await ViewModel!.InferSettingsFromModlistTxt(result.First().ToAbsolutePath());
});
}
}
}

View File

@ -1,8 +1,11 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Avalonia.Controls.Mixins;
using DynamicData;
@ -10,10 +13,12 @@ using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.App.Extensions;
using Wabbajack.App.Messages;
using Wabbajack.App.Models;
using Wabbajack.App.ViewModels;
using Wabbajack.Common;
using Wabbajack.Compiler;
using Wabbajack.DTOs;
using Wabbajack.DTOs.JsonConverters;
using Wabbajack.Installer;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
@ -23,6 +28,12 @@ namespace Wabbajack.App.Screens;
public class CompilerConfigurationViewModel : ViewModelBase, IReceiverMarker
{
private readonly DTOSerializer _dtos;
private readonly SettingsManager _settingsManager;
[Reactive]
public string Title { get; set; }
[Reactive]
public AbsolutePath SettingsFile { get; set; }
@ -33,7 +44,7 @@ public class CompilerConfigurationViewModel : ViewModelBase, IReceiverMarker
public GameMetaData BaseGame { get; set; }
[Reactive]
public AbsolutePath BasePath { get; set; }
public AbsolutePath Source { get; set; }
[Reactive]
public AbsolutePath GamePath { get; set; }
@ -53,96 +64,66 @@ public class CompilerConfigurationViewModel : ViewModelBase, IReceiverMarker
[Reactive]
public IEnumerable<RelativePath> AlwaysEnabled { get; set; } = Array.Empty<RelativePath>();
public AbsolutePath SettingsOutputLocation => Source.Combine(Title).WithExtension(IsMO2Compilation ? Ext.MO2CompilerSettings : Ext.CompilerSettings);
[Reactive]
public bool IsMO2Compilation { get; set; }
public CompilerConfigurationViewModel()
public CompilerConfigurationViewModel(DTOSerializer dtos, SettingsManager settingsManager)
{
_settingsManager = settingsManager;
_dtos = dtos;
Activator = new ViewModelActivator();
AllGames = GameRegistry.Games.Values.ToArray();
StartCompilation = ReactiveCommand.Create(() => BeginCompilation());
StartCompilation = ReactiveCommand.Create(() => BeginCompilation().FireAndForget());
OutputFolder = KnownFolders.EntryPoint;
this.WhenActivated(disposables =>
this.WhenActivated((CompositeDisposable 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);
LoadLastCompilation().FireAndForget();
});
}
private void BeginCompilation()
private async Task LoadLastCompilation()
{
var settings = new MO2CompilerSettings
var location = await _settingsManager.Load<AbsolutePath>("last_compilation");
if (location == default) return;
if (location.FileExists()) await LoadSettings(location);
}
private async Task BeginCompilation()
{
var settings = GetSettings();
await SaveSettingsFile();
await _settingsManager.Save("last_compilation", SettingsOutputLocation);
MessageBus.Instance.Send(new StartCompilation(settings));
MessageBus.Instance.Send(new NavigateTo(typeof(CompilationViewModel)));
}
private CompilerSettings GetSettings()
{
return new MO2CompilerSettings
{
Downloads = Downloads,
Source = BasePath,
Source = Source,
Game = BaseGame.Game,
Profile = SelectedProfile,
UseGamePaths = true,
OutputFile = OutputFolder.Combine(SelectedProfile).WithExtension(Ext.Wabbajack),
AlwaysEnabled = AlwaysEnabled.ToArray()
};
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;
}
public bool AddAlwaysExcluded(AbsolutePath path)
{
if (!path.InFolder(BasePath)) return false;
var relative = path.RelativeTo(BasePath);
if (!path.InFolder(Source)) return false;
var relative = path.RelativeTo(Source);
AlwaysEnabled = AlwaysEnabled.Append(relative).Distinct().ToArray();
return true;
}
@ -151,5 +132,90 @@ public class CompilerConfigurationViewModel : ViewModelBase, IReceiverMarker
{
AlwaysEnabled = AlwaysEnabled.Where(p => p != path).ToArray();
}
public async Task InferSettingsFromModlistTxt(AbsolutePath settingsFile)
{
if (settingsFile.FileName == "modlist.txt".ToRelativePath() && settingsFile.Depth > 3)
{
var mo2Folder = settingsFile.Parent.Parent.Parent;
var mo2Ini = mo2Folder.Combine(Consts.MO2IniName);
if (mo2Ini.FileExists())
{
var iniData = mo2Ini.LoadIniFile();
var general = iniData["General"];
BaseGame = GameRegistry.GetByFuzzyName(general["gameName"].FromMO2Ini());
Source = mo2Folder;
SelectedProfile = general["selected_profile"].FromMO2Ini();
GamePath = general["gamePath"].FromMO2Ini().ToAbsolutePath();
Title = SelectedProfile;
var settings = iniData["Settings"];
Downloads = settings["download_directory"].FromMO2Ini().ToAbsolutePath();
IsMO2Compilation = true;
// Find Always Enabled mods
foreach (var modFolder in mo2Folder.Combine("mods").EnumerateDirectories())
{
var iniFile = modFolder.Combine("meta.ini");
if (!iniFile.FileExists()) continue;
var data = iniFile.LoadIniFile();
var generalModData = data["General"];
if ((generalModData["notes"]?.Contains("WABBAJACK_ALWAYS_ENABLE") ?? false) ||
(generalModData["comments"]?.Contains("WABBAJACK_ALWAYS_ENABLE") ?? false))
{
AlwaysEnabled = AlwaysEnabled.Append(modFolder.RelativeTo(mo2Folder)).ToArray();
}
}
if (mo2Folder.Depth > 1)
OutputFolder = mo2Folder.Parent;
await SaveSettingsFile();
SettingsFile = SettingsOutputLocation;
}
}
}
private async Task SaveSettingsFile()
{
await using var st = SettingsOutputLocation.Open(FileMode.Create, FileAccess.Write, FileShare.None);
if (IsMO2Compilation)
await JsonSerializer.SerializeAsync(st, (MO2CompilerSettings)GetSettings(), _dtos.Options);
else
await JsonSerializer.SerializeAsync(st, GetSettings(), _dtos.Options);
}
private async Task LoadSettings(AbsolutePath path)
{
CompilerSettings s;
if (path.Extension == Ext.MO2CompilerSettings)
{
var mo2 = await LoadSettingsFile<MO2CompilerSettings>(path);
AlwaysEnabled = mo2.AlwaysEnabled;
SelectedProfile = mo2.Profile;
s = mo2;
}
else
{
throw new NotImplementedException();
}
Source = s.Source;
Downloads = s.Downloads;
OutputFolder = s.OutputFile.Depth > 1 ? s.OutputFile.Parent : s.OutputFile;
BaseGame = s.Game.MetaData();
}
private async Task<T> LoadSettingsFile<T>(AbsolutePath path)
{
await using var st = path.Open(FileMode.Open);
return (await JsonSerializer.DeserializeAsync<T>(st, _dtos.Options))!;
}
}

View File

@ -91,6 +91,8 @@ namespace Wabbajack.App
ModListsDownloadLocation = KnownFolders.EntryPoint.Combine("downloaded_mod_lists"),
SavedSettingsLocation = KnownFolders.WabbajackAppLocal.Combine("saved_settings")
});
services.AddSingleton<SettingsManager>();
services.AddSingleton(s =>
{

View File

@ -20,5 +20,8 @@ namespace Wabbajack.Common
public static Extension Json = new(".json");
public static Extension Md = new(".md");
public static Extension MetaData = new(".metadata");
public static Extension CompilerSettings = new(".compiler_settings");
public static Extension MO2CompilerSettings = new (".mo2_compiler_settings");
public static Extension Temp = new(".temp");
}
}

View File

@ -25,7 +25,7 @@ namespace Wabbajack.Compiler.CompilationSteps
.SelectMany(p => _mo2Compiler._settings.Source.Combine("profiles", p, "modlist.txt").ReadAllLines())
.Where(line => line.StartsWith("+") || line.EndsWith("_separator"))
.Select(line => line[1..].ToRelativePath().RelativeTo(_mo2Compiler.MO2ModsFolder))
//.Concat(alwaysEnabled)
.Concat(_mo2Compiler.Mo2Settings.AlwaysEnabled.Select(r => r.RelativeTo(_mo2Compiler.Settings.Source)))
//.Except(alwaysDisabled)
.ToList();
}

View File

@ -22,7 +22,7 @@ namespace Wabbajack.Compiler
{
public class MO2Compiler : ACompiler
{
private MO2CompilerSettings _mo2Settings => (MO2CompilerSettings)Settings;
public MO2CompilerSettings Mo2Settings => (MO2CompilerSettings)Settings;
public MO2Compiler(ILogger<MO2Compiler> logger, FileExtractor.FileExtractor extractor, FileHashCache hashCache, Context vfs,
TemporaryFileManager manager, MO2CompilerSettings settings, ParallelOptions parallelOptions, DownloadDispatcher dispatcher,
@ -31,12 +31,12 @@ namespace Wabbajack.Compiler
{
}
public AbsolutePath MO2ModsFolder => _settings.Source.Combine(Consts.MO2ModFolderName);
public AbsolutePath MO2ModsFolder => Settings.Source.Combine(Consts.MO2ModFolderName);
public IniData MO2Ini { get; }
public AbsolutePath MO2ProfileDir => _settings.Source.Combine(Consts.MO2Profiles, _mo2Settings.Profile);
public AbsolutePath MO2ProfileDir => Settings.Source.Combine(Consts.MO2Profiles, Mo2Settings.Profile);
public ConcurrentBag<Directive> ExtraFiles { get; private set; } = new();
public Dictionary<AbsolutePath, IniData> ModInis { get; set; } = new();
@ -47,19 +47,19 @@ namespace Wabbajack.Compiler
public override async Task<bool> Begin(CancellationToken token)
{
await _wjClient.SendMetric("begin_compiling", _mo2Settings.Profile);
await _wjClient.SendMetric("begin_compiling", Mo2Settings.Profile);
var roots = new List<AbsolutePath> {_settings.Source, _settings.Downloads};
roots.AddRange(_settings.OtherGames.Append(_settings.Game).Select(g => _locator.GameLocation(g)));
var roots = new List<AbsolutePath> {Settings.Source, Settings.Downloads};
roots.AddRange(Settings.OtherGames.Append(Settings.Game).Select(g => _locator.GameLocation(g)));
await _vfs.AddRoots(roots, token);
await InferMetas(token);
await _vfs.AddRoot(_settings.Downloads, token);
await _vfs.AddRoot(Settings.Downloads, token);
// Find all Downloads
IndexedArchives = await _settings.Downloads.EnumerateFiles()
IndexedArchives = await Settings.Downloads.EnumerateFiles()
.Where(f => f.WithExtension(Ext.Meta).FileExists())
.PMap(_parallelOptions,
async f => new IndexedArchive(_vfs.Index.ByRootPath[f])
@ -77,9 +77,9 @@ namespace Wabbajack.Compiler
await CleanInvalidArchivesAndFillState();
var mo2Files = _settings.Source.EnumerateFiles()
var mo2Files = Settings.Source.EnumerateFiles()
.Where(p => p.FileExists())
.Select(p => new RawSourceFile(_vfs.Index.ByRootPath[p], p.RelativeTo(_settings.Source)));
.Select(p => new RawSourceFile(_vfs.Index.ByRootPath[p], p.RelativeTo(Settings.Source)));
// If Game Folder Files exists, ignore the game folder
IndexedFiles = IndexedArchives.SelectMany(f => f.File.ThisAndAllChildren)
@ -101,7 +101,7 @@ namespace Wabbajack.Compiler
return false;
}
ModInis = _settings.Source.Combine(Consts.MO2ModFolderName)
ModInis = Settings.Source.Combine(Consts.MO2ModFolderName)
.EnumerateDirectories()
.Select(f =>
{
@ -147,18 +147,18 @@ namespace Wabbajack.Compiler
ModList = new ModList
{
GameType = _settings.Game,
GameType = Settings.Game,
WabbajackVersion = Consts.CurrentMinimumWabbajackVersion,
Archives = SelectedArchives.ToArray(),
Directives = InstallDirectives.ToArray(),
Name = _settings.ModListName,
Author = _settings.ModListAuthor,
Description = _settings.ModListDescription,
Readme = _settings.ModlistReadme,
Name = Settings.ModListName,
Author = Settings.ModListAuthor,
Description = Settings.ModListDescription,
Readme = Settings.ModlistReadme,
Image = ModListImage != default ? ModListImage.FileName : default,
Website = _settings.ModListWebsite,
Version = _settings.ModlistVersion,
IsNSFW = _settings.ModlistIsNSFW
Website = Settings.ModListWebsite,
Version = Settings.ModlistVersion,
IsNSFW = Settings.ModlistIsNSFW
};
await InlineFiles(token);
@ -242,7 +242,7 @@ namespace Wabbajack.Compiler
new IncludeRegex(this, "^[^\\\\]*\\.bat$"),
new IncludeModIniData(this),
new DirectMatch(this),
new IncludeTaggedFiles(this, _settings.Include),
new IncludeTaggedFiles(this, Settings.Include),
// TODO: rework tagged files
// new IncludeTaggedFolders(this, Consts.WABBAJACK_INCLUDE),
new IgnoreExtension(this, Ext.Pyc),
@ -272,7 +272,7 @@ namespace Wabbajack.Compiler
// TODO
//new zEditIntegration.IncludeZEditPatches(this),
new IncludeTaggedFiles(this, _settings.NoMatchInclude),
new IncludeTaggedFiles(this, Settings.NoMatchInclude),
new IncludeRegex(this, ".*\\.txt"),
new IgnorePathContains(this,@"\Edit Scripts\Export\"),
new IgnoreExtension(this, new Extension(".CACHE")),