From f919789f52823169829bca0f4c23770f263d6427 Mon Sep 17 00:00:00 2001 From: Joshua Askharoun Date: Sat, 19 Feb 2022 10:26:32 -0600 Subject: [PATCH 1/5] Switched to .NET Core version of WindowsAPICodePack --- ZuneModdingHelper/ZuneModdingHelper.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ZuneModdingHelper/ZuneModdingHelper.csproj b/ZuneModdingHelper/ZuneModdingHelper.csproj index be5627b..4652d03 100644 --- a/ZuneModdingHelper/ZuneModdingHelper.csproj +++ b/ZuneModdingHelper/ZuneModdingHelper.csproj @@ -26,7 +26,7 @@ - + From 65bd5f4e8a6a534f7aab8fd265b5d02855fdbb15 Mon Sep 17 00:00:00 2001 From: Joshua Askharoun Date: Tue, 22 Feb 2022 14:32:20 -0600 Subject: [PATCH 2/5] Added entrance animation to home page --- .../Behaviors/FadeAnimateItemsBehavior.cs | 75 +++++++++++++++++++ ZuneModdingHelper/MainWindow.xaml | 10 +++ ZuneModdingHelper/MainWindow.xaml.cs | 7 +- 3 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 ZuneModdingHelper/Behaviors/FadeAnimateItemsBehavior.cs diff --git a/ZuneModdingHelper/Behaviors/FadeAnimateItemsBehavior.cs b/ZuneModdingHelper/Behaviors/FadeAnimateItemsBehavior.cs new file mode 100644 index 0000000..6163ba5 --- /dev/null +++ b/ZuneModdingHelper/Behaviors/FadeAnimateItemsBehavior.cs @@ -0,0 +1,75 @@ +using Microsoft.Xaml.Behaviors; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Windows.Controls; +using System.Windows.Media.Animation; +using System.Windows.Threading; + +namespace ZuneModdingHelper.Behaviors +{ + // https://stackoverflow.com/a/5910833 + public class FadeAnimateItemsBehavior : Behavior + { + public DoubleAnimation Animation { get; set; } + public TimeSpan Tick { get; set; } + + protected override void OnAttached() + { + base.OnAttached(); + AssociatedObject.Loaded += AssociatedObject_Loaded; + } + + void AssociatedObject_Loaded(object sender, System.Windows.RoutedEventArgs e) + { + IEnumerable items; + if (AssociatedObject.ItemsSource == null) + { + items = AssociatedObject.Items.Cast(); + } + else + { + var itemsSource = AssociatedObject.ItemsSource; + if (itemsSource is INotifyCollectionChanged) + { + var collection = itemsSource as INotifyCollectionChanged; + collection.CollectionChanged += (s, cce) => + { + if (cce.Action == NotifyCollectionChangedAction.Add) + { + var itemContainer = AssociatedObject.ItemContainerGenerator.ContainerFromItem(cce.NewItems[0]) as ListBoxItem; + itemContainer.BeginAnimation(ListBoxItem.OpacityProperty, Animation); + } + }; + + } + ListBoxItem[] itemsSub = new ListBoxItem[AssociatedObject.Items.Count]; + for (int i = 0; i < itemsSub.Length; i++) + { + itemsSub[i] = AssociatedObject.ItemContainerGenerator.ContainerFromIndex(i) as ListBoxItem; + } + items = itemsSub; + } + foreach (var item in items) + { + item.Opacity = 0; + } + var enumerator = items.GetEnumerator(); + if (enumerator.MoveNext()) + { + DispatcherTimer timer = new() { Interval = Tick }; + timer.Tick += (s, timerE) => + { + var item = enumerator.Current; + item.BeginAnimation(ListBoxItem.OpacityProperty, Animation); + if (!enumerator.MoveNext()) + { + timer.Stop(); + } + }; + timer.Start(); + } + } + } +} diff --git a/ZuneModdingHelper/MainWindow.xaml b/ZuneModdingHelper/MainWindow.xaml index 7b08b9b..b9c24f0 100644 --- a/ZuneModdingHelper/MainWindow.xaml +++ b/ZuneModdingHelper/MainWindow.xaml @@ -6,6 +6,8 @@ xmlns:local="clr-namespace:ZuneModdingHelper" xmlns:core="clr-namespace:ZuneModCore;assembly=ZuneModCore" xmlns:mah="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro" + xmlns:i="http://schemas.microsoft.com/xaml/behaviors" + xmlns:b="clr-namespace:ZuneModdingHelper.Behaviors" mc:Ignorable="d" Title="{x:Static local:App.Title}" Height="450" Width="800" WindowStartupLocation="CenterScreen" Loaded="Window_Loaded"> @@ -131,6 +133,14 @@ + + + + + + + + diff --git a/ZuneModdingHelper/MainWindow.xaml.cs b/ZuneModdingHelper/MainWindow.xaml.cs index ddfaea3..800b929 100644 --- a/ZuneModdingHelper/MainWindow.xaml.cs +++ b/ZuneModdingHelper/MainWindow.xaml.cs @@ -34,20 +34,21 @@ namespace ZuneModdingHelper public MainWindow() { InitializeComponent(); + // https://github.com/Arlodotexe/OwlCore/issues/1 OwlCore.Threading.SetPrimarySynchronizationContext(System.Threading.SynchronizationContext.Current!); OwlCore.Threading.SetPrimaryThreadInvokeHandler(RunOnUI); ThemeManager.Current.ThemeSyncMode = ThemeSyncMode.SyncWithAppMode; ThemeManager.Current.SyncTheme(); + + ModList.ItemsSource = Mod.AvailableMods; + ZuneInstallDirBox.Text = Mod.ZuneInstallDir; } private async System.Threading.Tasks.Task RunOnUI(Action action) => await Dispatcher.BeginInvoke(action); private async void Window_Loaded(object sender, RoutedEventArgs e) { - ModList.ItemsSource = Mod.AvailableMods; - ZuneInstallDirBox.Text = Mod.ZuneInstallDir; - // Show a warning if Zune is running Process[] procs = Process.GetProcessesByName("Zune"); string zuneExePath = Path.Combine(Mod.ZuneInstallDir, "Zune.exe"); From 1acf068092e65a3e581c62744ea0abc20cde5231 Mon Sep 17 00:00:00 2001 From: Joshua Askharoun Date: Tue, 1 Mar 2022 14:14:11 -0600 Subject: [PATCH 3/5] Initial mod dependency work --- .../Controls/AbstractUIGroupPresenter.cs | 59 +------------- .../Themes/AbstractUIGroupPresenterStyle.xaml | 1 + ZuneModCore/Mod.cs | 77 ++++++++++++++---- ZuneModCore/ModDependency.cs | 31 +++++++ ZuneModCore/ModManager.cs | 80 +++++++++++++++++++ ZuneModCore/Mods/BackgroundImageMod.cs | 6 +- ZuneModCore/Mods/FeaturesOverrideMod.cs | 2 +- ZuneModCore/Mods/MbidLocatorMod.cs | 2 +- ZuneModCore/Mods/VideoSyncMod.cs | 2 +- ZuneModCore/Mods/WebservicesMod.cs | 11 ++- .../ReleaseVersion.cs | 22 +++-- ZuneModCore/StatusModel.cs | 20 +++++ ZuneModCore/ZuneModCore.csproj | 1 + .../AbstractUIGroupDialog.xaml.cs | 43 ---------- ZuneModdingHelper/App.xaml.cs | 3 +- ZuneModdingHelper/MainWindow.xaml.cs | 17 ++-- ...IGroupDialog.xaml => OptionsUIDialog.xaml} | 31 ++++--- ZuneModdingHelper/OptionsUIDialog.xaml.cs | 57 +++++++++++++ 18 files changed, 310 insertions(+), 155 deletions(-) create mode 100644 ZuneModCore/ModDependency.cs create mode 100644 ZuneModCore/ModManager.cs rename {ZuneModdingHelper => ZuneModCore}/ReleaseVersion.cs (96%) create mode 100644 ZuneModCore/StatusModel.cs delete mode 100644 ZuneModdingHelper/AbstractUIGroupDialog.xaml.cs rename ZuneModdingHelper/{AbstractUIGroupDialog.xaml => OptionsUIDialog.xaml} (65%) create mode 100644 ZuneModdingHelper/OptionsUIDialog.xaml.cs diff --git a/OwlCore.Wpf/AbstractUI/Controls/AbstractUIGroupPresenter.cs b/OwlCore.Wpf/AbstractUI/Controls/AbstractUIGroupPresenter.cs index aee2838..45840fb 100644 --- a/OwlCore.Wpf/AbstractUI/Controls/AbstractUIGroupPresenter.cs +++ b/OwlCore.Wpf/AbstractUI/Controls/AbstractUIGroupPresenter.cs @@ -1,7 +1,6 @@ using System.Windows; using System.Windows.Controls; using OwlCore.AbstractUI.ViewModels; -using OwlCore.AbstractUI.Models; namespace OwlCore.Wpf.AbstractUI.Controls { @@ -16,7 +15,7 @@ namespace OwlCore.Wpf.AbstractUI.Controls /// Backing property for . /// public static readonly DependencyProperty ViewModelProperty = - DependencyProperty.Register(nameof(ViewModel), typeof(AbstractUICollectionViewModel), typeof(AbstractUIGroupPresenter), new PropertyMetadata(null, (d, e) => ((AbstractUIGroupPresenter)d).OnViewModelChanged())); + DependencyProperty.Register(nameof(ViewModel), typeof(AbstractUICollectionViewModel), typeof(AbstractUIGroupPresenter), new PropertyMetadata(null)); /// /// Backing property for . @@ -48,62 +47,6 @@ namespace OwlCore.Wpf.AbstractUI.Controls public AbstractUIGroupPresenter() { this.DefaultStyleKey = typeof(AbstractUIGroupPresenter); - - AttachEvents(); - } - - private void AttachEvents() - { - Loaded += OnLoaded; - - DataContextChanged += OnDataContextChanged; - } - - private void DetachEvents() - { - DataContextChanged -= OnDataContextChanged; - } - - private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e) - { - if (_dataContextBeingSet) - return; - - _dataContextBeingSet = true; - - if (DataContext is AbstractUICollection elementGroup) - ViewModel = new AbstractUICollectionViewModel(elementGroup); - - if (DataContext is AbstractUICollectionViewModel elementGroupViewModel) - ViewModel = elementGroupViewModel; - - _dataContextBeingSet = false; - } - - private void OnLoaded(object sender, RoutedEventArgs e) - { - Loaded -= OnLoaded; - Unloaded += OnUnloaded; - } - - private void OnUnloaded(object sender, RoutedEventArgs e) - { - Unloaded -= OnUnloaded; - - DetachEvents(); - } - - /// - /// Raised when the changes. - /// - public void OnViewModelChanged() - { - if (_dataContextBeingSet) - return; - - _dataContextBeingSet = true; - DataContext = ViewModel; - _dataContextBeingSet = false; } } } diff --git a/OwlCore.Wpf/AbstractUI/Themes/AbstractUIGroupPresenterStyle.xaml b/OwlCore.Wpf/AbstractUI/Themes/AbstractUIGroupPresenterStyle.xaml index dcb65fa..f7ec8c3 100644 --- a/OwlCore.Wpf/AbstractUI/Themes/AbstractUIGroupPresenterStyle.xaml +++ b/OwlCore.Wpf/AbstractUI/Themes/AbstractUIGroupPresenterStyle.xaml @@ -85,6 +85,7 @@ diff --git a/ZuneModCore/Mod.cs b/ZuneModCore/Mod.cs index f7692b4..9c6aaef 100644 --- a/ZuneModCore/Mod.cs +++ b/ZuneModCore/Mod.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using ZuneModCore.Mods; @@ -9,20 +10,6 @@ namespace ZuneModCore { public abstract class Mod { - /// - /// A list of all available mods - /// - public static readonly IReadOnlyList AvailableMods = new List - { - new FeaturesOverrideMod(), - new VideoSyncMod(), - new WebservicesMod(), - new BackgroundImageMod(), - new MbidLocatorMod(), - }.AsReadOnly(); - - public static string ZuneInstallDir { get; set; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Zune"); - public abstract string Id { get; } public abstract string Title { get; } @@ -55,13 +42,71 @@ namespace ZuneModCore { get { - string dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ZuneModCore", Id); + string dir = Path.Combine(ModManager.CoreStorageDir, Id); // Create the directory just in case the consumer assumes the folder exists already Directory.CreateDirectory(dir); return dir; } } - public abstract IReadOnlyList? DependentMods { get; } + public abstract IReadOnlyList? DependentMods { get; } + + /// + /// Initializes any missing depedencies. + /// + public Task InitDependencies() => ForAllDependencies(async mod => + { + bool isDepApplied = await mod.CheckApplied(); + if (!isDepApplied) + await mod.Init(); + }); + + /// + /// Applies any missing depedencies. + /// + /// + /// Make sure to call first. + /// + public Task ApplyDependencies() => ForAllDependencies(async mod => + { + bool isDepApplied = await mod.CheckApplied(); + if (!isDepApplied) + await mod.Apply(); + }); + + public Task CheckApplied() => TrueForAllDependencies(mod => mod.CheckApplied()); + + private async Task ForAllDependencies(Func asyncAction) + { + // 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) + await asyncAction(depMod); + } + } + + private async Task TrueForAllDependencies(Func> asyncAction) + { + // Check if there are any dependencies + if (DependentMods == null) + return true; + + foreach (var dep in DependentMods) + { + var depMod = ModManager.AvailableMods.First(m => m.Id == dep.Id); + if (depMod != null) + if (!await asyncAction(depMod)) + return false; + } + + return true; + } + + public override string ToString() => Title; } } diff --git a/ZuneModCore/ModDependency.cs b/ZuneModCore/ModDependency.cs new file mode 100644 index 0000000..a87ffaa --- /dev/null +++ b/ZuneModCore/ModDependency.cs @@ -0,0 +1,31 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace ZuneModCore +{ + public class ModDependency + { + public string Id { get; set; } + + public ReleaseVersion Version { get; set; } + + public ModDependency(string id, ReleaseVersion? version = null) + { + Id = id; + Version = version ?? ModManager.CurrentVersion; + } + + /// + /// DO NOT USE DIRECTLY. This constructor is only for + /// JSON de/serialization. + /// + [Obsolete] + internal ModDependency() { } + + public Task CheckStatus() => ModManager.CheckStatus(Id); + + public override string ToString() => $"{Id} / {Version}"; + } +} diff --git a/ZuneModCore/ModManager.cs b/ZuneModCore/ModManager.cs new file mode 100644 index 0000000..c54c9c0 --- /dev/null +++ b/ZuneModCore/ModManager.cs @@ -0,0 +1,80 @@ +using CommunityToolkit.Diagnostics; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; +using ZuneModCore.Mods; + +namespace ZuneModCore +{ + public static class ModManager + { + /// + /// A list of all available mods. + /// + public static readonly IReadOnlyList AvailableMods = new List + { + new FeaturesOverrideMod(), + new VideoSyncMod(), + new WebservicesMod(), + new BackgroundImageMod(), + new MbidLocatorMod(), + }; + + public static string ZuneInstallDir { get; set; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Zune"); + + public static readonly ReleaseVersion CurrentVersion = new(2021, 12, 30, 0, Phase.Alpha); + + internal static readonly string CoreStorageDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ZuneModCore"); + + private static readonly string CoreStatusFile = Path.Combine(CoreStorageDir, "ModStatus.json"); + + /// + /// Marks the specified mod as applied in the status file. + /// + internal static Task MarkApplied(string modId) => MarkStatus(modId, true); + + /// + /// Marks the specified mod as reset in the status file. + /// + internal static Task MarkReset(string modId) => MarkStatus(modId, false); + + /// + /// Sets the status of the specified mod in the status file. + /// + private static async Task MarkStatus(string modId, bool status) + { + using var stream = File.Open(CoreStatusFile, FileMode.OpenOrCreate); + var model = await JsonSerializer.DeserializeAsync(stream); + Guard.IsNotNull(model, nameof(model)); + + bool curStatus = model.InstalledMods.ContainsKey(modId); + if (curStatus && !status) + { + model.InstalledMods.Remove(modId); + } + else if (!curStatus && status) + { + model.InstalledMods.Add(modId, CurrentVersion); + } + + await JsonSerializer.SerializeAsync(stream, model); + } + + /// + /// Gets the status of the specified mod from the status file. + /// + internal static async Task CheckStatus(string modId) + { + if (!File.Exists(CoreStatusFile)) + return false; + + using var stream = File.OpenRead(CoreStatusFile); + var model = await JsonSerializer.DeserializeAsync(stream); + Guard.IsNotNull(model, nameof(model)); + + return model.InstalledMods.ContainsKey(modId); + } + } +} diff --git a/ZuneModCore/Mods/BackgroundImageMod.cs b/ZuneModCore/Mods/BackgroundImageMod.cs index e8df83d..accaa6c 100644 --- a/ZuneModCore/Mods/BackgroundImageMod.cs +++ b/ZuneModCore/Mods/BackgroundImageMod.cs @@ -32,7 +32,7 @@ namespace ZuneModCore.Mods }; } - public override IReadOnlyList? DependentMods => null; + public override IReadOnlyList? DependentMods => null; public override async Task Apply() { @@ -43,7 +43,7 @@ namespace ZuneModCore.Mods return $"The file '{bgimgInfo.FullName}' does not exist."; } - FileInfo zsrDllInfo = new(Path.Combine(ZuneInstallDir, "ZuneShellResources.dll")); + FileInfo zsrDllInfo = new(Path.Combine(ModManager.ZuneInstallDir, "ZuneShellResources.dll")); if (!zsrDllInfo.Exists) { return $"The file '{zsrDllInfo.FullName}' does not exist."; @@ -120,7 +120,7 @@ namespace ZuneModCore.Mods { // Copy backup to application folder File.Copy(Path.Combine(StorageDirectory, "ZuneShellResources.original.dll"), - Path.Combine(ZuneInstallDir, "ZuneShellResources.dll"), true); + Path.Combine(ModManager.ZuneInstallDir, "ZuneShellResources.dll"), true); return Task.FromResult(null); } diff --git a/ZuneModCore/Mods/FeaturesOverrideMod.cs b/ZuneModCore/Mods/FeaturesOverrideMod.cs index 72e5542..c9da497 100644 --- a/ZuneModCore/Mods/FeaturesOverrideMod.cs +++ b/ZuneModCore/Mods/FeaturesOverrideMod.cs @@ -56,7 +56,7 @@ namespace ZuneModCore.Mods }; } - public override IReadOnlyList? DependentMods => null; + public override IReadOnlyList? DependentMods => null; public override Task Init() { diff --git a/ZuneModCore/Mods/MbidLocatorMod.cs b/ZuneModCore/Mods/MbidLocatorMod.cs index 8aada30..e580c59 100644 --- a/ZuneModCore/Mods/MbidLocatorMod.cs +++ b/ZuneModCore/Mods/MbidLocatorMod.cs @@ -37,7 +37,7 @@ namespace ZuneModCore.Mods }; } - public override IReadOnlyList? DependentMods => null; + public override IReadOnlyList? DependentMods => null; public override async Task Apply() { diff --git a/ZuneModCore/Mods/VideoSyncMod.cs b/ZuneModCore/Mods/VideoSyncMod.cs index b651773..da549e1 100644 --- a/ZuneModCore/Mods/VideoSyncMod.cs +++ b/ZuneModCore/Mods/VideoSyncMod.cs @@ -71,6 +71,6 @@ namespace ZuneModCore.Mods } } - public override IReadOnlyList? DependentMods => null; + public override IReadOnlyList? DependentMods => null; } } diff --git a/ZuneModCore/Mods/WebservicesMod.cs b/ZuneModCore/Mods/WebservicesMod.cs index 4513915..3a508f4 100644 --- a/ZuneModCore/Mods/WebservicesMod.cs +++ b/ZuneModCore/Mods/WebservicesMod.cs @@ -76,7 +76,10 @@ namespace ZuneModCore.Mods }; } - public override IReadOnlyList? DependentMods => null; + public override IReadOnlyList? DependentMods => new List + { + new(nameof(FeaturesOverrideMod)) + }; public override Task Init() { @@ -90,7 +93,7 @@ namespace ZuneModCore.Mods public override async Task Apply() { // Verify that ZuneServices.dll exists - FileInfo zsDllInfo = new(Path.Combine(ZuneInstallDir, "ZuneService.dll")); + FileInfo zsDllInfo = new(Path.Combine(ModManager.ZuneInstallDir, "ZuneService.dll")); if (!zsDllInfo.Exists) { return $"The file '{zsDllInfo.FullName}' does not exist."; @@ -182,7 +185,7 @@ namespace ZuneModCore.Mods public override async Task Reset() { - string zsDllPath = Path.Combine(ZuneInstallDir, "ZuneService.dll"); + string zsDllPath = Path.Combine(ModManager.ZuneInstallDir, "ZuneService.dll"); try { // Copy backup to application folder @@ -258,7 +261,7 @@ namespace ZuneModCore.Mods } } - private async Task Ping(string url) + private static async Task Ping(string url) { try { diff --git a/ZuneModdingHelper/ReleaseVersion.cs b/ZuneModCore/ReleaseVersion.cs similarity index 96% rename from ZuneModdingHelper/ReleaseVersion.cs rename to ZuneModCore/ReleaseVersion.cs index 26dd726..e5c460c 100644 --- a/ZuneModdingHelper/ReleaseVersion.cs +++ b/ZuneModCore/ReleaseVersion.cs @@ -1,14 +1,13 @@ using CommunityToolkit.Diagnostics; using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; +using System.Text.Json; +using System.Text.Json.Serialization; -namespace ZuneModdingHelper +namespace ZuneModCore { + [JsonConverter(typeof(ReleaseVersionJsonConverter))] public sealed class ReleaseVersion : ICloneable, IComparable, IComparable, IEquatable { /// @@ -481,4 +480,17 @@ namespace ZuneModdingHelper ReleaseCandidate, Production } + + internal class ReleaseVersionJsonConverter : JsonConverter + { + public override ReleaseVersion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return ReleaseVersion.Parse(reader.GetString()!); + } + + public override void Write(Utf8JsonWriter writer, ReleaseVersion value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } + } } diff --git a/ZuneModCore/StatusModel.cs b/ZuneModCore/StatusModel.cs new file mode 100644 index 0000000..9db0cdc --- /dev/null +++ b/ZuneModCore/StatusModel.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace ZuneModCore +{ + internal class StatusModel + { + /// + /// The version of ZuneModCore that created this model. + /// + public ReleaseVersion Version { get; set; } + + /// + /// A list of mods that are currently installed. + /// + /// Key is mod ID and Value is installed version. + /// + /// + public Dictionary InstalledMods { get; set; } + } +} diff --git a/ZuneModCore/ZuneModCore.csproj b/ZuneModCore/ZuneModCore.csproj index 4f3daa8..22e89bf 100644 --- a/ZuneModCore/ZuneModCore.csproj +++ b/ZuneModCore/ZuneModCore.csproj @@ -8,6 +8,7 @@ + diff --git a/ZuneModdingHelper/AbstractUIGroupDialog.xaml.cs b/ZuneModdingHelper/AbstractUIGroupDialog.xaml.cs deleted file mode 100644 index b2aa71a..0000000 --- a/ZuneModdingHelper/AbstractUIGroupDialog.xaml.cs +++ /dev/null @@ -1,43 +0,0 @@ -using MahApps.Metro.Controls; -using OwlCore.AbstractUI.Models; -using OwlCore.AbstractUI.ViewModels; -using OwlCore.Wpf.AbstractUI.Controls; -using System.Windows; - -namespace ZuneModdingHelper -{ - /// - /// Interaction logic for AbstractUIGroupDialog.xaml - /// - public partial class AbstractUIGroupDialog : MetroWindow - { - public AbstractUIGroupDialog(AbstractUICollectionViewModel viewModel) - { - ViewModel = viewModel; - DataContext = ViewModel; - InitializeComponent(); - } - public AbstractUIGroupDialog(AbstractUICollection group) : this(new AbstractUICollectionViewModel(group)) - { - - } - - private void OptionsUINextButton_Click(object sender, RoutedEventArgs e) => Finish(true); - - private void OptionsUICancelButton_Click(object sender, RoutedEventArgs e) => Finish(false); - - private void Finish(bool result) - { - DialogResult = result; - Close(); - } - - public AbstractUICollectionViewModel ViewModel - { - get => (AbstractUICollectionViewModel)GetValue(ViewModelProperty); - set => SetValue(ViewModelProperty, value); - } - public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register( - nameof(ViewModel), typeof(AbstractUICollectionViewModel), typeof(AbstractUIGroupDialog)); - } -} diff --git a/ZuneModdingHelper/App.xaml.cs b/ZuneModdingHelper/App.xaml.cs index 74feb34..8f69b9e 100644 --- a/ZuneModdingHelper/App.xaml.cs +++ b/ZuneModdingHelper/App.xaml.cs @@ -2,6 +2,7 @@ using Microsoft.AppCenter; using Microsoft.AppCenter.Analytics; using Microsoft.AppCenter.Crashes; +using ZuneModCore; namespace ZuneModdingHelper { @@ -12,7 +13,7 @@ namespace ZuneModdingHelper { public const string Title = "Zune Modding Helper"; - public static readonly ReleaseVersion Version = new(2021, 12, 30, 0, Phase.Alpha); + public static readonly ReleaseVersion Version = ModManager.CurrentVersion; public static readonly string VersionStr = Version.ToString(); public const string DonateLink = "http://josh.askharoun.com/donate"; diff --git a/ZuneModdingHelper/MainWindow.xaml.cs b/ZuneModdingHelper/MainWindow.xaml.cs index 800b929..5c01a6c 100644 --- a/ZuneModdingHelper/MainWindow.xaml.cs +++ b/ZuneModdingHelper/MainWindow.xaml.cs @@ -41,8 +41,8 @@ namespace ZuneModdingHelper ThemeManager.Current.ThemeSyncMode = ThemeSyncMode.SyncWithAppMode; ThemeManager.Current.SyncTheme(); - ModList.ItemsSource = Mod.AvailableMods; - ZuneInstallDirBox.Text = Mod.ZuneInstallDir; + ModList.ItemsSource = ModManager.AvailableMods; + ZuneInstallDirBox.Text = ModManager.ZuneInstallDir; } private async System.Threading.Tasks.Task RunOnUI(Action action) => await Dispatcher.BeginInvoke(action); @@ -51,7 +51,7 @@ namespace ZuneModdingHelper { // Show a warning if Zune is running Process[] procs = Process.GetProcessesByName("Zune"); - string zuneExePath = Path.Combine(Mod.ZuneInstallDir, "Zune.exe"); + string zuneExePath = Path.Combine(ModManager.ZuneInstallDir, "Zune.exe"); if (procs.Length > 0 && procs.Any(p => p.MainModule.FileName == zuneExePath)) { await this.ShowMessageAsync( @@ -65,7 +65,7 @@ namespace ZuneModdingHelper { var progDialog = await this.ShowProgressAsync("Getting ready...", "Preparing to apply mod", settings: defaultMetroDialogSettings); Mod mod = (Mod)((FrameworkElement)sender).DataContext; - Mod.ZuneInstallDir = ZuneInstallDirBox.Text; + ModManager.ZuneInstallDir = ZuneInstallDirBox.Text; progDialog.Maximum = 3; int numCompleted = 0; @@ -80,15 +80,14 @@ namespace ZuneModdingHelper progDialog.SetMessage("Awaiting options..."); if (mod.OptionsUI != null) { - var optionsDialog = new AbstractUIGroupDialog(mod.OptionsUI); - optionsDialog.Title = optionsDialog.Title + " | " + mod.Title; + var optionsDialog = new OptionsUIDialog(mod); bool? optionsResult = optionsDialog.ShowDialog(); if (!(optionsResult.HasValue && optionsResult.Value)) { await progDialog.CloseAsync(); return; } - mod.OptionsUI = (AbstractUICollection)optionsDialog.ViewModel.Model; + mod.OptionsUI = (AbstractUICollection)optionsDialog.OptionsViewModel.Model; } progDialog.SetProgress(++numCompleted); @@ -122,7 +121,7 @@ namespace ZuneModdingHelper if (sender is FrameworkElement elem && elem.DataContext is Mod mod) { var progDialog = await this.ShowProgressAsync("Getting ready...", "Preparing to reset mod", settings: defaultMetroDialogSettings); - Mod.ZuneInstallDir = ZuneInstallDirBox.Text; + ModManager.ZuneInstallDir = ZuneInstallDirBox.Text; progDialog.Maximum = 2; int numCompleted = 0; @@ -279,7 +278,7 @@ namespace ZuneModdingHelper CommonOpenFileDialog dialog = new() { IsFolderPicker = true, - DefaultDirectory = Mod.ZuneInstallDir + DefaultDirectory = ModManager.ZuneInstallDir }; CommonFileDialogResult result = dialog.ShowDialog(); if (result == CommonFileDialogResult.Ok) diff --git a/ZuneModdingHelper/AbstractUIGroupDialog.xaml b/ZuneModdingHelper/OptionsUIDialog.xaml similarity index 65% rename from ZuneModdingHelper/AbstractUIGroupDialog.xaml rename to ZuneModdingHelper/OptionsUIDialog.xaml index 0fc3e2d..f88138f 100644 --- a/ZuneModdingHelper/AbstractUIGroupDialog.xaml +++ b/ZuneModdingHelper/OptionsUIDialog.xaml @@ -1,14 +1,14 @@ - + @@ -24,8 +24,13 @@ - + + + + + diff --git a/ZuneModdingHelper/OptionsUIDialog.xaml.cs b/ZuneModdingHelper/OptionsUIDialog.xaml.cs new file mode 100644 index 0000000..6ccb4e2 --- /dev/null +++ b/ZuneModdingHelper/OptionsUIDialog.xaml.cs @@ -0,0 +1,57 @@ +using MahApps.Metro.Controls; +using OwlCore.AbstractUI.Models; +using OwlCore.AbstractUI.ViewModels; +using OwlCore.Wpf.AbstractUI.Controls; +using System.Windows; +using ZuneModCore; + +namespace ZuneModdingHelper +{ + /// + /// A dialog that displays the options and dependencies for the given . + /// + public partial class OptionsUIDialog : MetroWindow + { + public OptionsUIDialog(Mod mod) + { + InitializeComponent(); + + DataContext = this; + CurrentMod = mod; + if (CurrentMod.OptionsUI != null) + { + OptionsViewModel = new(CurrentMod.OptionsUI); + } + + Title = "Options | " + CurrentMod.Title; + } + + private void OptionsUINextButton_Click(object sender, RoutedEventArgs e) => Finish(true); + + private void OptionsUICancelButton_Click(object sender, RoutedEventArgs e) => Finish(false); + + private void Finish(bool result) + { + DialogResult = result; + Close(); + } + + public Mod CurrentMod + { + get => (Mod)GetValue(CurrentModProperty); + set => SetValue(CurrentModProperty, value); + } + public static readonly DependencyProperty CurrentModProperty = DependencyProperty.Register( + nameof(CurrentMod), typeof(Mod), typeof(OptionsUIDialog)); + + public AbstractUICollectionViewModel OptionsViewModel + { + get => (AbstractUICollectionViewModel)GetValue(OptionsViewModelProperty); + set => SetValue(OptionsViewModelProperty, value); + } + public static readonly DependencyProperty OptionsViewModelProperty = DependencyProperty.Register( + nameof(OptionsViewModel), typeof(AbstractUICollectionViewModel), typeof(OptionsUIDialog)); + + public bool HasOptions => CurrentMod.OptionsUI != null; + } +} From 7ca0440095967cdb790e3a0b531217ba38fdea84 Mon Sep 17 00:00:00 2001 From: Joshua Askharoun Date: Tue, 1 Mar 2022 15:09:26 -0600 Subject: [PATCH 4/5] Added UI for dependencies --- ZuneModCore/ModDependency.cs | 14 ++++--- .../Converters/BoolToSymbolConverter.cs | 40 +++++++++++++++++++ .../Converters/NullToVisibilityConverter.cs | 20 ++++++++++ ZuneModdingHelper/OptionsUIDialog.xaml | 27 +++++++++++-- 4 files changed, 93 insertions(+), 8 deletions(-) create mode 100644 ZuneModdingHelper/Converters/BoolToSymbolConverter.cs create mode 100644 ZuneModdingHelper/Converters/NullToVisibilityConverter.cs diff --git a/ZuneModCore/ModDependency.cs b/ZuneModCore/ModDependency.cs index a87ffaa..412923f 100644 --- a/ZuneModCore/ModDependency.cs +++ b/ZuneModCore/ModDependency.cs @@ -7,16 +7,18 @@ namespace ZuneModCore { public class ModDependency { - public string Id { get; set; } - - public ReleaseVersion Version { get; set; } - public ModDependency(string id, ReleaseVersion? version = null) { Id = id; Version = version ?? ModManager.CurrentVersion; } + public string Id { get; set; } + + public ReleaseVersion Version { get; set; } + + public bool Status => CheckStatus(); + /// /// DO NOT USE DIRECTLY. This constructor is only for /// JSON de/serialization. @@ -24,7 +26,9 @@ namespace ZuneModCore [Obsolete] internal ModDependency() { } - public Task CheckStatus() => ModManager.CheckStatus(Id); + public Task CheckStatusAsync() => ModManager.CheckStatus(Id); + + private bool CheckStatus() => CheckStatusAsync().Result; public override string ToString() => $"{Id} / {Version}"; } diff --git a/ZuneModdingHelper/Converters/BoolToSymbolConverter.cs b/ZuneModdingHelper/Converters/BoolToSymbolConverter.cs new file mode 100644 index 0000000..528a0e0 --- /dev/null +++ b/ZuneModdingHelper/Converters/BoolToSymbolConverter.cs @@ -0,0 +1,40 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using System.Windows.Media; + +namespace ZuneModdingHelper.Converters +{ + internal class BoolToSymbolConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is not bool val) + return null; + + var resultsStr = parameter.ToString().Split(','); + object trueReturnVal = null; + object falseReturnVal = null; + + if (targetType == typeof(string)) + { + trueReturnVal = resultsStr[0]; + falseReturnVal = resultsStr[1]; + } + else if (targetType == typeof(Brush)) + { + var brushConverter = new BrushConverter(); + trueReturnVal = brushConverter.ConvertFrom(resultsStr[0]); + falseReturnVal = brushConverter.ConvertFrom(resultsStr[1]); + } + + return val ? trueReturnVal : falseReturnVal; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + var results = parameter.ToString().Split(','); + return value.ToString() == results[0]; + } + } +} diff --git a/ZuneModdingHelper/Converters/NullToVisibilityConverter.cs b/ZuneModdingHelper/Converters/NullToVisibilityConverter.cs new file mode 100644 index 0000000..ff21ef5 --- /dev/null +++ b/ZuneModdingHelper/Converters/NullToVisibilityConverter.cs @@ -0,0 +1,20 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace ZuneModdingHelper.Converters +{ + internal class NullToVisibilityConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value != null ? Visibility.Visible : Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/ZuneModdingHelper/OptionsUIDialog.xaml b/ZuneModdingHelper/OptionsUIDialog.xaml index f88138f..329951a 100644 --- a/ZuneModdingHelper/OptionsUIDialog.xaml +++ b/ZuneModdingHelper/OptionsUIDialog.xaml @@ -4,6 +4,8 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:ZuneModdingHelper" + xmlns:convert="clr-namespace:ZuneModdingHelper.Converters" + xmlns:mod="clr-namespace:ZuneModCore;assembly=ZuneModCore" xmlns:absui="clr-namespace:OwlCore.Wpf.AbstractUI.Controls;assembly=OwlCore.Wpf" xmlns:mah="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro" mc:Ignorable="d" @@ -14,6 +16,9 @@ + + + @@ -25,11 +30,27 @@ + + + + + + + + + + + + + + + + - - From 1ea5810d5d3abf41c115e920b2071f3226a9c8ad Mon Sep 17 00:00:00 2001 From: Joshua Askharoun Date: Thu, 3 Mar 2022 14:26:05 -0600 Subject: [PATCH 5/5] Better apply/reset flow in backend --- ZuneModCore/Mod.cs | 88 +++++++++++++++-- ZuneModCore/ModDependency.cs | 5 +- ZuneModCore/ModManager.cs | 43 ++++++--- ZuneModCore/ModViewModel.cs | 112 ++++++++++++++++++++++ ZuneModCore/Mods/BackgroundImageMod.cs | 4 +- ZuneModCore/Mods/FeaturesOverrideMod.cs | 21 +++- ZuneModCore/Mods/MbidLocatorMod.cs | 4 +- ZuneModCore/Mods/VideoSyncMod.cs | 9 +- ZuneModCore/Mods/WebservicesMod.cs | 4 +- ZuneModCore/Win32/RegEdit.cs | 42 ++++++++ ZuneModdingHelper/MainWindow.xaml.cs | 11 ++- ZuneModdingHelper/OptionsUIDialog.xaml | 15 +-- ZuneModdingHelper/OptionsUIDialog.xaml.cs | 27 ++---- 13 files changed, 316 insertions(+), 69 deletions(-) create mode 100644 ZuneModCore/ModViewModel.cs diff --git a/ZuneModCore/Mod.cs b/ZuneModCore/Mod.cs index 9c6aaef..ed734b0 100644 --- a/ZuneModCore/Mod.cs +++ b/ZuneModCore/Mod.cs @@ -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 Apply(); + protected abstract Task ApplyCore(); - public abstract Task Reset(); + protected abstract Task 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; } + /// + /// Gets a directory that mods can use to store mod-specific information, + /// such as backups for resetting. + /// public string StorageDirectory { get @@ -51,12 +60,29 @@ namespace ZuneModCore public abstract IReadOnlyList? DependentMods { get; } + /// + /// Applies this mod, without any of its dependencies. + /// + /// + /// The error message if not successful. + /// + public async Task Apply() + { + string? error = await ApplyCore(); + if (error == null) + { + ModManager.MarkApplied(Id); + FireStatusChanged(true); + } + return error; + } + /// /// Initializes any missing depedencies. /// - 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 /// /// Make sure to call first. /// - 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 CheckApplied() => TrueForAllDependencies(mod => mod.CheckApplied()); + /// + /// Applies this mod and all its dependencies. + /// + /// + /// The error message if not successful. + /// + public async Task ApplyWithDependencies() + { + await ApplyDependencies(); + return await Apply(); + } - private async Task ForAllDependencies(Func asyncAction) + /// + /// Resets this mod, without resetting any dependencies. + /// + public async Task 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 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 asyncAction) { // Check if there are any dependencies if (DependentMods == null) @@ -90,7 +158,7 @@ namespace ZuneModCore } } - private async Task TrueForAllDependencies(Func> asyncAction) + private bool TrueForAllDependencies(Func 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; } diff --git a/ZuneModCore/ModDependency.cs b/ZuneModCore/ModDependency.cs index 412923f..04d598c 100644 --- a/ZuneModCore/ModDependency.cs +++ b/ZuneModCore/ModDependency.cs @@ -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 CheckStatusAsync() => ModManager.CheckStatus(Id); - - private bool CheckStatus() => CheckStatusAsync().Result; + public bool CheckStatus() => ModManager.CheckStatus(Id); public override string ToString() => $"{Id} / {Version}"; } diff --git a/ZuneModCore/ModManager.cs b/ZuneModCore/ModManager.cs index c54c9c0..b0c724d 100644 --- a/ZuneModCore/ModManager.cs +++ b/ZuneModCore/ModManager.cs @@ -33,21 +33,19 @@ namespace ZuneModCore /// /// Marks the specified mod as applied in the status file. /// - internal static Task MarkApplied(string modId) => MarkStatus(modId, true); + internal static void MarkApplied(string modId) => MarkStatus(modId, true); /// /// Marks the specified mod as reset in the status file. /// - internal static Task MarkReset(string modId) => MarkStatus(modId, false); + internal static void MarkReset(string modId) => MarkStatus(modId, false); /// /// Sets the status of the specified mod in the status file. /// - 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(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); } /// /// Gets the status of the specified mod from the status file. /// - internal static async Task 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(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(json); + } + + model ??= new() + { + Version = CurrentVersion, + InstalledMods = new() + }; + + Guard.IsNotNull(model, nameof(model)); + return model; + } } } diff --git a/ZuneModCore/ModViewModel.cs b/ZuneModCore/ModViewModel.cs new file mode 100644 index 0000000..38c5905 --- /dev/null +++ b/ZuneModCore/ModViewModel.cs @@ -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; + } + } +} diff --git a/ZuneModCore/Mods/BackgroundImageMod.cs b/ZuneModCore/Mods/BackgroundImageMod.cs index accaa6c..0621b19 100644 --- a/ZuneModCore/Mods/BackgroundImageMod.cs +++ b/ZuneModCore/Mods/BackgroundImageMod.cs @@ -34,7 +34,7 @@ namespace ZuneModCore.Mods public override IReadOnlyList? DependentMods => null; - public override async Task Apply() + protected override async Task ApplyCore() { string bgimgPath = ((AbstractTextBox)OptionsUI!.Items[0]).Value; FileInfo bgimgInfo = new(bgimgPath); @@ -114,7 +114,7 @@ namespace ZuneModCore.Mods } } - public override Task Reset() + protected override Task ResetCore() { try { diff --git a/ZuneModCore/Mods/FeaturesOverrideMod.cs b/ZuneModCore/Mods/FeaturesOverrideMod.cs index c9da497..d27c28a 100644 --- a/ZuneModCore/Mods/FeaturesOverrideMod.cs +++ b/ZuneModCore/Mods/FeaturesOverrideMod.cs @@ -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 Apply() => await Apply(false); + protected override async Task ApplyCore() => await Apply(false); public async Task 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 Reset() + protected override async Task 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); diff --git a/ZuneModCore/Mods/MbidLocatorMod.cs b/ZuneModCore/Mods/MbidLocatorMod.cs index e580c59..1333c48 100644 --- a/ZuneModCore/Mods/MbidLocatorMod.cs +++ b/ZuneModCore/Mods/MbidLocatorMod.cs @@ -39,7 +39,7 @@ namespace ZuneModCore.Mods public override IReadOnlyList? DependentMods => null; - public override async Task Apply() + protected override async Task 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 Reset() + protected override Task ResetCore() { return Task.FromResult(null); } diff --git a/ZuneModCore/Mods/VideoSyncMod.cs b/ZuneModCore/Mods/VideoSyncMod.cs index da549e1..e1d9570 100644 --- a/ZuneModCore/Mods/VideoSyncMod.cs +++ b/ZuneModCore/Mods/VideoSyncMod.cs @@ -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 Apply() + protected override async Task ApplyCore() { // Make a backup of the original file FileVersionInfo wmvDllVersionInfo = FileVersionInfo.GetVersionInfo(WMVCORE_PATH); @@ -56,7 +53,7 @@ namespace ZuneModCore.Mods } } - public override Task Reset() + protected override Task ResetCore() { try { diff --git a/ZuneModCore/Mods/WebservicesMod.cs b/ZuneModCore/Mods/WebservicesMod.cs index 3a508f4..b786374 100644 --- a/ZuneModCore/Mods/WebservicesMod.cs +++ b/ZuneModCore/Mods/WebservicesMod.cs @@ -90,7 +90,7 @@ namespace ZuneModCore.Mods return Task.CompletedTask; } - public override async Task Apply() + protected override async Task 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 Reset() + protected override async Task ResetCore() { string zsDllPath = Path.Combine(ModManager.ZuneInstallDir, "ZuneService.dll"); try diff --git a/ZuneModCore/Win32/RegEdit.cs b/ZuneModCore/Win32/RegEdit.cs index 6f75735..50c9e77 100644 --- a/ZuneModCore/Win32/RegEdit.cs +++ b/ZuneModCore/Win32/RegEdit.cs @@ -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 diff --git a/ZuneModdingHelper/MainWindow.xaml.cs b/ZuneModdingHelper/MainWindow.xaml.cs index 5c01a6c..06baf43 100644 --- a/ZuneModdingHelper/MainWindow.xaml.cs +++ b/ZuneModdingHelper/MainWindow.xaml.cs @@ -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); diff --git a/ZuneModdingHelper/OptionsUIDialog.xaml b/ZuneModdingHelper/OptionsUIDialog.xaml index 329951a..b73c639 100644 --- a/ZuneModdingHelper/OptionsUIDialog.xaml +++ b/ZuneModdingHelper/OptionsUIDialog.xaml @@ -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"> @@ -30,9 +31,9 @@ - + - + @@ -49,8 +50,9 @@ Height="2" Margin="0,10,0,-7"/> - + @@ -80,7 +82,8 @@