Better apply/reset flow in backend

This commit is contained in:
Joshua Askharoun
2022-03-03 14:26:05 -06:00
parent 7ca0440095
commit 1ea5810d5d
13 changed files with 316 additions and 69 deletions
+78 -10
View File
@@ -18,13 +18,18 @@ namespace ZuneModCore
public abstract string Author { get; }
public delegate void StatusChangedHandler(Mod sender, bool status);
public event StatusChangedHandler? StatusChanged;
public virtual AbstractUICollection? GetDefaultOptionsUI() => null;
public virtual Task Init() => Task.CompletedTask;
public abstract Task<string?> Apply();
protected abstract Task<string?> ApplyCore();
public abstract Task<string?> Reset();
protected abstract Task<string?> ResetCore();
protected virtual void FireStatusChanged(bool status) => StatusChanged?.Invoke(this, status);
private AbstractUICollection? _OptionsUI;
public AbstractUICollection? OptionsUI
@@ -38,6 +43,10 @@ namespace ZuneModCore
set => _OptionsUI = value;
}
/// <summary>
/// Gets a directory that mods can use to store mod-specific information,
/// such as backups for resetting.
/// </summary>
public string StorageDirectory
{
get
@@ -51,12 +60,29 @@ namespace ZuneModCore
public abstract IReadOnlyList<ModDependency>? DependentMods { get; }
/// <summary>
/// Applies this mod, without any of its dependencies.
/// </summary>
/// <returns>
/// The error message if not successful.
/// </returns>
public async Task<string?> Apply()
{
string? error = await ApplyCore();
if (error == null)
{
ModManager.MarkApplied(Id);
FireStatusChanged(true);
}
return error;
}
/// <summary>
/// Initializes any missing depedencies.
/// </summary>
public Task InitDependencies() => ForAllDependencies(async mod =>
public Task InitDependencies() => ForAllDependenciesAsync(async mod =>
{
bool isDepApplied = await mod.CheckApplied();
bool isDepApplied = mod.CheckApplied();
if (!isDepApplied)
await mod.Init();
});
@@ -67,16 +93,58 @@ namespace ZuneModCore
/// <remarks>
/// Make sure to call <see cref="InitDependencies"/> first.
/// </remarks>
public Task ApplyDependencies() => ForAllDependencies(async mod =>
public Task ApplyDependencies() => ForAllDependenciesAsync(async mod =>
{
bool isDepApplied = await mod.CheckApplied();
bool isDepApplied = mod.CheckApplied();
if (!isDepApplied)
await mod.Apply();
});
public Task<bool> CheckApplied() => TrueForAllDependencies(mod => mod.CheckApplied());
/// <summary>
/// Applies this mod and all its dependencies.
/// </summary>
/// <returns>
/// The error message if not successful.
/// </returns>
public async Task<string?> ApplyWithDependencies()
{
await ApplyDependencies();
return await Apply();
}
private async Task ForAllDependencies(Func<Mod, Task> asyncAction)
/// <summary>
/// Resets this mod, without resetting any dependencies.
/// </summary>
public async Task<string?> Reset()
{
string? error = await ResetCore();
if (error == null)
{
ModManager.MarkReset(Id);
FireStatusChanged(false);
}
return error;
}
public bool CheckApplied() => ModManager.CheckStatus(Id);
public bool CheckDependenciesApplied() => TrueForAllDependencies(mod => mod.CheckApplied());
private void ForAllDependencies(Action<Mod> action)
{
// Check if there are any dependencies
if (DependentMods == null)
return;
foreach (var dep in DependentMods)
{
var depMod = ModManager.AvailableMods.First(m => m.Id == dep.Id);
if (depMod != null)
action(depMod);
}
}
private async Task ForAllDependenciesAsync(Func<Mod, Task> asyncAction)
{
// Check if there are any dependencies
if (DependentMods == null)
@@ -90,7 +158,7 @@ namespace ZuneModCore
}
}
private async Task<bool> TrueForAllDependencies(Func<Mod, Task<bool>> asyncAction)
private bool TrueForAllDependencies(Func<Mod, bool> action)
{
// Check if there are any dependencies
if (DependentMods == null)
@@ -100,7 +168,7 @@ namespace ZuneModCore
{
var depMod = ModManager.AvailableMods.First(m => m.Id == dep.Id);
if (depMod != null)
if (!await asyncAction(depMod))
if (!action(depMod))
return false;
}
+2 -3
View File
@@ -1,4 +1,5 @@
using System;
using System.ComponentModel;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
@@ -26,9 +27,7 @@ namespace ZuneModCore
[Obsolete]
internal ModDependency() { }
public Task<bool> CheckStatusAsync() => ModManager.CheckStatus(Id);
private bool CheckStatus() => CheckStatusAsync().Result;
public bool CheckStatus() => ModManager.CheckStatus(Id);
public override string ToString() => $"{Id} / {Version}";
}
+28 -15
View File
@@ -33,21 +33,19 @@ namespace ZuneModCore
/// <summary>
/// Marks the specified mod as applied in the status file.
/// </summary>
internal static Task MarkApplied(string modId) => MarkStatus(modId, true);
internal static void MarkApplied(string modId) => MarkStatus(modId, true);
/// <summary>
/// Marks the specified mod as reset in the status file.
/// </summary>
internal static Task MarkReset(string modId) => MarkStatus(modId, false);
internal static void MarkReset(string modId) => MarkStatus(modId, false);
/// <summary>
/// Sets the status of the specified mod in the status file.
/// </summary>
private static async Task MarkStatus(string modId, bool status)
private static void MarkStatus(string modId, bool status)
{
using var stream = File.Open(CoreStatusFile, FileMode.OpenOrCreate);
var model = await JsonSerializer.DeserializeAsync<StatusModel>(stream);
Guard.IsNotNull(model, nameof(model));
var model = GetOrCreateStatusModel();
bool curStatus = model.InstalledMods.ContainsKey(modId);
if (curStatus && !status)
@@ -59,22 +57,37 @@ namespace ZuneModCore
model.InstalledMods.Add(modId, CurrentVersion);
}
await JsonSerializer.SerializeAsync(stream, model);
string json = JsonSerializer.Serialize(model);
File.WriteAllText(CoreStatusFile, json);
}
/// <summary>
/// Gets the status of the specified mod from the status file.
/// </summary>
internal static async Task<bool> CheckStatus(string modId)
internal static bool CheckStatus(string modId)
{
if (!File.Exists(CoreStatusFile))
return false;
using var stream = File.OpenRead(CoreStatusFile);
var model = await JsonSerializer.DeserializeAsync<StatusModel>(stream);
Guard.IsNotNull(model, nameof(model));
var model = GetOrCreateStatusModel();
return model.InstalledMods.ContainsKey(modId);
}
private static StatusModel GetOrCreateStatusModel()
{
StatusModel? model = null;
if (File.Exists(CoreStatusFile))
{
string json = File.ReadAllText(CoreStatusFile);
if (!string.IsNullOrWhiteSpace(json))
model = JsonSerializer.Deserialize<StatusModel>(json);
}
model ??= new()
{
Version = CurrentVersion,
InstalledMods = new()
};
Guard.IsNotNull(model, nameof(model));
return model;
}
}
}
+112
View File
@@ -0,0 +1,112 @@
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
using OwlCore.AbstractUI.Models;
using OwlCore.AbstractUI.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ZuneModCore
{
public class ModViewModel : ObservableObject, IDisposable
{
public ModViewModel(Mod mod)
{
Mod = mod;
_loadCommand = new AsyncRelayCommand(LoadAsync);
_applyCommand = new AsyncRelayCommand(Mod.ApplyWithDependencies);
_resetCommand = new AsyncRelayCommand(Mod.Reset);
if (Mod.OptionsUI != null)
OptionsViewModel = new(Mod.OptionsUI);
Mod.StatusChanged += OnStatusChanged;
}
private Mod _mod;
private AbstractUICollectionViewModel? _optionsVm;
private IAsyncRelayCommand _loadCommand;
private IAsyncRelayCommand? _actionCommand;
private bool _hasOptions;
private bool _hasDependencies;
private string _actionButtonText;
private readonly IAsyncRelayCommand _applyCommand;
private readonly IAsyncRelayCommand _resetCommand;
public Mod Mod
{
get => _mod;
set
{
HasDependencies = _mod?.DependentMods != null && _mod.DependentMods.Count > 0;
SetProperty(ref _mod, value);
}
}
public AbstractUICollectionViewModel? OptionsViewModel
{
get => _optionsVm;
set
{
HasOptions = _optionsVm != null;
SetProperty(ref _optionsVm, value);
}
}
public IAsyncRelayCommand LoadCommand
{
get => _loadCommand;
set => SetProperty(ref _loadCommand, value);
}
public IAsyncRelayCommand? ActionCommand
{
get => _actionCommand;
set => SetProperty(ref _actionCommand, value);
}
public bool HasOptions
{
get => _hasOptions;
set => SetProperty(ref _hasOptions, value);
}
public bool HasDependencies
{
get => _hasDependencies;
set => SetProperty(ref _hasDependencies, value);
}
public string ActionButtonText
{
get => _actionButtonText;
set => SetProperty(ref _actionButtonText, value);
}
public async Task LoadAsync()
{
OnStatusChanged(_mod, _mod.CheckApplied());
}
private void OnStatusChanged(Mod mod, bool status)
{
if (status)
{
ActionButtonText = "Reset";
ActionCommand = _resetCommand;
}
else
{
ActionButtonText = "Apply";
ActionCommand = _applyCommand;
}
}
public void Dispose()
{
Mod.StatusChanged -= OnStatusChanged;
}
}
}
+2 -2
View File
@@ -34,7 +34,7 @@ namespace ZuneModCore.Mods
public override IReadOnlyList<ModDependency>? DependentMods => null;
public override async Task<string?> Apply()
protected override async Task<string?> ApplyCore()
{
string bgimgPath = ((AbstractTextBox)OptionsUI!.Items[0]).Value;
FileInfo bgimgInfo = new(bgimgPath);
@@ -114,7 +114,7 @@ namespace ZuneModCore.Mods
}
}
public override Task<string?> Reset()
protected override Task<string?> ResetCore()
{
try
{
+18 -3
View File
@@ -1,6 +1,7 @@
using OwlCore.AbstractUI.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using ZuneModCore.Win32;
@@ -72,10 +73,13 @@ namespace ZuneModCore.Mods
return Task.CompletedTask;
}
public override async Task<string?> Apply() => await Apply(false);
protected override async Task<string?> ApplyCore() => await Apply(false);
public async Task<string?> Apply(bool applyAll = false)
{
// Save backup of current values
ExportFeatureOverrides(Path.Combine(StorageDirectory, "FeatureOverrides.reg"));
// Use user choices from AbstractUI
foreach (AbstractUIElement uiElem in OptionsUI!.Items)
{
@@ -84,7 +88,7 @@ namespace ZuneModCore.Mods
bool isSuccess = SetFeatureOverride(boolElem.Id, true);
if (!isSuccess)
{
string? resetStatus = await Reset();
string? resetStatus = await ResetCore();
if (resetStatus != null)
{
// The reset failed as well, return both errors
@@ -101,13 +105,24 @@ namespace ZuneModCore.Mods
return null;
}
public override async Task<string?> Reset()
protected override async Task<string?> ResetCore()
{
// Delete all values first, so if a backup exists it has a clean slate
RegEdit.CurrentUserDeleteKey(ZUNE_FEATURESOVERRIDE_REGKEY);
FileInfo reg = new(Path.Combine(StorageDirectory, "FeatureOverrides.reg"));
if (reg.Exists)
{
// Load backup of original values
RegEdit.ImportKey(reg.FullName);
}
return null;
}
public static void ExportFeatureOverrides(string path) =>
RegEdit.ExportKey(RegEdit.HIVE_CURRENTUSER, ZUNE_FEATURESOVERRIDE_REGKEY, path);
public static bool SetFeatureOverride(string feature, bool value) =>
RegEdit.CurrentUserSetBoolValue(ZUNE_FEATURESOVERRIDE_REGKEY, feature, value);
+2 -2
View File
@@ -39,7 +39,7 @@ namespace ZuneModCore.Mods
public override IReadOnlyList<ModDependency>? DependentMods => null;
public override async Task<string?> Apply()
protected override async Task<string?> ApplyCore()
{
// Use user choices from AbstractUI
string folderPath = ((AbstractTextBox)OptionsUI!.Items[0]).Value;
@@ -72,7 +72,7 @@ namespace ZuneModCore.Mods
return null;
}
public override Task<string?> Reset()
protected override Task<string?> ResetCore()
{
return Task.FromResult<string?>(null);
}
+3 -6
View File
@@ -1,10 +1,7 @@
using OwlCore.AbstractUI.Models;
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Threading.Tasks;
using ZuneModCore.Win32;
@@ -23,7 +20,7 @@ namespace ZuneModCore.Mods
public override string Id => nameof(VideoSyncMod);
public override async Task<string?> Apply()
protected override async Task<string?> ApplyCore()
{
// Make a backup of the original file
FileVersionInfo wmvDllVersionInfo = FileVersionInfo.GetVersionInfo(WMVCORE_PATH);
@@ -56,7 +53,7 @@ namespace ZuneModCore.Mods
}
}
public override Task<string?> Reset()
protected override Task<string?> ResetCore()
{
try
{
+2 -2
View File
@@ -90,7 +90,7 @@ namespace ZuneModCore.Mods
return Task.CompletedTask;
}
public override async Task<string?> Apply()
protected override async Task<string?> ApplyCore()
{
// Verify that ZuneServices.dll exists
FileInfo zsDllInfo = new(Path.Combine(ModManager.ZuneInstallDir, "ZuneService.dll"));
@@ -183,7 +183,7 @@ namespace ZuneModCore.Mods
}
}
public override async Task<string?> Reset()
protected override async Task<string?> ResetCore()
{
string zsDllPath = Path.Combine(ModManager.ZuneInstallDir, "ZuneService.dll");
try
+42
View File
@@ -1,4 +1,5 @@
using Microsoft.Win32;
using System.Diagnostics;
namespace ZuneModCore.Win32
{
@@ -7,6 +8,7 @@ namespace ZuneModCore.Win32
public class RegEdit
{
public const string HIVE_CURRENTUSER = "HKEY_CURRENT_USER";
public const string ZUNE_REG_PATH = @"SOFTWARE\Microsoft\Zune\";
public static bool CurrentUserSetBoolValue(string key, string name, bool value)
@@ -66,6 +68,46 @@ namespace ZuneModCore.Win32
regKey.DeleteValue(name, false);
}
public static void ExportKey(string hive, string regKey, string savePath, bool overwrite = true)
{
string path = $"\"{savePath}\"";
string key = $"\"{hive}\\{regKey}\"";
Process proc = null!;
try
{
string cmd = $"reg export {key} {path}";
if (overwrite)
cmd += " /y";
proc = Process.Start("cmd.exe", $"/c {cmd}");
if (proc != null) proc.WaitForExit();
}
finally
{
if (proc != null) proc.Dispose();
}
}
public static void ImportKey(string savePath)
{
string path = "\"" + savePath + "\"";
Process proc = null!;
try
{
string cmd = $"reg import {path}";
proc = Process.Start("cmd.exe", $"/c \"{cmd}\"");
if (proc != null) proc.WaitForExit();
}
finally
{
if (proc != null) proc.Dispose();
}
}
}
#pragma warning restore CA1416 // Validate platform compatibility
+9 -2
View File
@@ -54,10 +54,17 @@ namespace ZuneModdingHelper
string zuneExePath = Path.Combine(ModManager.ZuneInstallDir, "Zune.exe");
if (procs.Length > 0 && procs.Any(p => p.MainModule.FileName == zuneExePath))
{
MetroDialogSettings dialogSettings = new()
{
ColorScheme = MetroDialogColorScheme.Accented,
AnimateShow = true,
AnimateHide = true,
AffirmativeButtonText = "OK"
};
await this.ShowMessageAsync(
"Warning",
"The Zune software is currently running. You may run into issues applying or resetting mods.",
settings: defaultMetroDialogSettings);
settings: dialogSettings);
}
}
@@ -87,7 +94,7 @@ namespace ZuneModdingHelper
await progDialog.CloseAsync();
return;
}
mod.OptionsUI = (AbstractUICollection)optionsDialog.OptionsViewModel.Model;
//mod.OptionsUI = (AbstractUICollection)optionsDialog.ViewModel.Model;
}
progDialog.SetProgress(++numCompleted);
+9 -6
View File
@@ -10,7 +10,8 @@
xmlns:mah="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
mc:Ignorable="d"
Title="Options" Height="450" Width="800"
WindowStyle="ToolWindow">
WindowStyle="ToolWindow"
Loaded="Window_Loaded">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
@@ -30,9 +31,9 @@
<ScrollViewer>
<StackPanel Orientation="Vertical">
<StackPanel Margin="10,10,10,0" Visibility="{Binding CurrentMod.DependentMods, Converter={StaticResource NullToVisibilityConverter}}">
<StackPanel Margin="10,10,10,0" Visibility="{Binding ViewModel.HasDependencies}">
<TextBlock Text="Dependencies" FontSize="26"/>
<ItemsControl x:Name="DependenciesPresenter" ItemsSource="{Binding CurrentMod.DependentMods}">
<ItemsControl x:Name="DependenciesPresenter" ItemsSource="{Binding ViewModel.Mod.DependentMods}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type mod:ModDependency}">
<TextBlock Margin="4" FontSize="14">
@@ -49,8 +50,9 @@
Height="2" Margin="0,10,0,-7"/>
</StackPanel>
<absui:AbstractUIGroupPresenter x:Name="OptionsUIPresenter" ViewModel="{Binding OptionsViewModel}"
TemplateSelector="{StaticResource GroupTemplateSelector}"/>
<absui:AbstractUIGroupPresenter x:Name="OptionsUIPresenter" ViewModel="{Binding ViewModel.OptionsViewModel}"
TemplateSelector="{StaticResource GroupTemplateSelector}"
Visibility="{Binding ViewModel.HasOptions}"/>
</StackPanel>
</ScrollViewer>
@@ -80,7 +82,8 @@
<Button x:Name="OptionsUICancelButton" Content="Cancel" Click="OptionsUICancelButton_Click"
Grid.Column="1" Style="{StaticResource SecondaryButton}"/>
<Button x:Name="OptionsUINextButton" Content="Next" Click="OptionsUINextButton_Click"
<Button x:Name="OptionsUINextButton" Content="{Binding ViewModel.ActionButtonText}"
Command="{Binding ViewModel.ActionCommand}"
Grid.Column="2" Style="{StaticResource PrimaryButton}"/>
</Grid>
</Border>
+9 -18
View File
@@ -17,13 +17,9 @@ namespace ZuneModdingHelper
InitializeComponent();
DataContext = this;
CurrentMod = mod;
if (CurrentMod.OptionsUI != null)
{
OptionsViewModel = new(CurrentMod.OptionsUI);
}
ViewModel = new(mod);
Title = "Options | " + CurrentMod.Title;
Title = "Options | " + ViewModel.Mod.Title;
}
private void OptionsUINextButton_Click(object sender, RoutedEventArgs e) => Finish(true);
@@ -36,22 +32,17 @@ namespace ZuneModdingHelper
Close();
}
public Mod CurrentMod
public ModViewModel ViewModel
{
get => (Mod)GetValue(CurrentModProperty);
set => SetValue(CurrentModProperty, value);
get => (ModViewModel)GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
public static readonly DependencyProperty CurrentModProperty = DependencyProperty.Register(
nameof(CurrentMod), typeof(Mod), typeof(OptionsUIDialog));
public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(
nameof(ViewModel), typeof(ModViewModel), typeof(OptionsUIDialog));
public AbstractUICollectionViewModel OptionsViewModel
private async void Window_Loaded(object sender, RoutedEventArgs e)
{
get => (AbstractUICollectionViewModel)GetValue(OptionsViewModelProperty);
set => SetValue(OptionsViewModelProperty, value);
await ViewModel.LoadAsync();
}
public static readonly DependencyProperty OptionsViewModelProperty = DependencyProperty.Register(
nameof(OptionsViewModel), typeof(AbstractUICollectionViewModel), typeof(OptionsUIDialog));
public bool HasOptions => CurrentMod.OptionsUI != null;
}
}