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..ed734b0 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; }
@@ -31,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
@@ -51,17 +43,138 @@ 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
{
- 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; }
+
+ ///
+ /// 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() => ForAllDependenciesAsync(async mod =>
+ {
+ bool isDepApplied = mod.CheckApplied();
+ if (!isDepApplied)
+ await mod.Init();
+ });
+
+ ///
+ /// Applies any missing depedencies.
+ ///
+ ///
+ /// Make sure to call first.
+ ///
+ public Task ApplyDependencies() => ForAllDependenciesAsync(async mod =>
+ {
+ bool isDepApplied = mod.CheckApplied();
+ if (!isDepApplied)
+ await mod.Apply();
+ });
+
+ ///
+ /// Applies this mod and all its dependencies.
+ ///
+ ///
+ /// The error message if not successful.
+ ///
+ public async Task ApplyWithDependencies()
+ {
+ await ApplyDependencies();
+ return await Apply();
+ }
+
+ ///
+ /// 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)
+ return;
+
+ foreach (var dep in DependentMods)
+ {
+ var depMod = ModManager.AvailableMods.First(m => m.Id == dep.Id);
+ if (depMod != null)
+ await asyncAction(depMod);
+ }
+ }
+
+ private bool TrueForAllDependencies(Func action)
+ {
+ // 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 (!action(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..04d598c
--- /dev/null
+++ b/ZuneModCore/ModDependency.cs
@@ -0,0 +1,34 @@
+using System;
+using System.ComponentModel;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+
+namespace ZuneModCore
+{
+ public class ModDependency
+ {
+ 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.
+ ///
+ [Obsolete]
+ internal ModDependency() { }
+
+ public bool 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..b0c724d
--- /dev/null
+++ b/ZuneModCore/ModManager.cs
@@ -0,0 +1,93 @@
+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 void MarkApplied(string modId) => MarkStatus(modId, true);
+
+ ///
+ /// Marks the specified mod as reset in the status file.
+ ///
+ internal static void MarkReset(string modId) => MarkStatus(modId, false);
+
+ ///
+ /// Sets the status of the specified mod in the status file.
+ ///
+ private static void MarkStatus(string modId, bool status)
+ {
+ var model = GetOrCreateStatusModel();
+
+ bool curStatus = model.InstalledMods.ContainsKey(modId);
+ if (curStatus && !status)
+ {
+ model.InstalledMods.Remove(modId);
+ }
+ else if (!curStatus && status)
+ {
+ model.InstalledMods.Add(modId, CurrentVersion);
+ }
+
+ string json = JsonSerializer.Serialize(model);
+ File.WriteAllText(CoreStatusFile, json);
+ }
+
+ ///
+ /// Gets the status of the specified mod from the status file.
+ ///
+ internal static bool CheckStatus(string modId)
+ {
+ 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 e8df83d..0621b19 100644
--- a/ZuneModCore/Mods/BackgroundImageMod.cs
+++ b/ZuneModCore/Mods/BackgroundImageMod.cs
@@ -32,9 +32,9 @@ namespace ZuneModCore.Mods
};
}
- public override IReadOnlyList? DependentMods => null;
+ 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);
@@ -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.";
@@ -114,13 +114,13 @@ namespace ZuneModCore.Mods
}
}
- public override Task Reset()
+ protected override Task ResetCore()
{
try
{
// 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..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;
@@ -56,7 +57,7 @@ namespace ZuneModCore.Mods
};
}
- public override IReadOnlyList? DependentMods => null;
+ public override IReadOnlyList? DependentMods => null;
public override Task Init()
{
@@ -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 c57a6b0..07ef150 100644
--- a/ZuneModCore/Mods/MbidLocatorMod.cs
+++ b/ZuneModCore/Mods/MbidLocatorMod.cs
@@ -42,9 +42,9 @@ namespace ZuneModCore.Mods
};
}
- public override IReadOnlyList? DependentMods => null;
+ 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;
@@ -82,7 +82,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 b651773..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
{
@@ -71,6 +68,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..b786374 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()
{
@@ -87,10 +90,10 @@ 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(ZuneInstallDir, "ZuneService.dll"));
+ FileInfo zsDllInfo = new(Path.Combine(ModManager.ZuneInstallDir, "ZuneService.dll"));
if (!zsDllInfo.Exists)
{
return $"The file '{zsDllInfo.FullName}' does not exist.";
@@ -180,9 +183,9 @@ namespace ZuneModCore.Mods
}
}
- public override async Task Reset()
+ protected override async Task ResetCore()
{
- 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/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/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 b/ZuneModdingHelper/AbstractUIGroupDialog.xaml
deleted file mode 100644
index 0fc3e2d..0000000
--- a/ZuneModdingHelper/AbstractUIGroupDialog.xaml
+++ /dev/null
@@ -1,62 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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/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/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/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..06baf43 100644
--- a/ZuneModdingHelper/MainWindow.xaml.cs
+++ b/ZuneModdingHelper/MainWindow.xaml.cs
@@ -34,29 +34,37 @@ 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 = ModManager.AvailableMods;
+ ZuneInstallDirBox.Text = ModManager.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");
+ 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);
}
}
@@ -64,7 +72,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;
@@ -79,15 +87,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.ViewModel.Model;
}
progDialog.SetProgress(++numCompleted);
@@ -121,7 +128,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;
@@ -278,7 +285,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/OptionsUIDialog.xaml b/ZuneModdingHelper/OptionsUIDialog.xaml
new file mode 100644
index 0000000..b73c639
--- /dev/null
+++ b/ZuneModdingHelper/OptionsUIDialog.xaml
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ZuneModdingHelper/AbstractUIGroupDialog.xaml.cs b/ZuneModdingHelper/OptionsUIDialog.xaml.cs
similarity index 56%
rename from ZuneModdingHelper/AbstractUIGroupDialog.xaml.cs
rename to ZuneModdingHelper/OptionsUIDialog.xaml.cs
index b2aa71a..c646ce9 100644
--- a/ZuneModdingHelper/AbstractUIGroupDialog.xaml.cs
+++ b/ZuneModdingHelper/OptionsUIDialog.xaml.cs
@@ -3,23 +3,23 @@ using OwlCore.AbstractUI.Models;
using OwlCore.AbstractUI.ViewModels;
using OwlCore.Wpf.AbstractUI.Controls;
using System.Windows;
+using ZuneModCore;
namespace ZuneModdingHelper
{
///
- /// Interaction logic for AbstractUIGroupDialog.xaml
+ /// A dialog that displays the options and dependencies for the given .
///
- public partial class AbstractUIGroupDialog : MetroWindow
+ public partial class OptionsUIDialog : MetroWindow
{
- public AbstractUIGroupDialog(AbstractUICollectionViewModel viewModel)
+ public OptionsUIDialog(Mod mod)
{
- ViewModel = viewModel;
- DataContext = ViewModel;
InitializeComponent();
- }
- public AbstractUIGroupDialog(AbstractUICollection group) : this(new AbstractUICollectionViewModel(group))
- {
+ DataContext = this;
+ ViewModel = new(mod);
+
+ Title = "Options | " + ViewModel.Mod.Title;
}
private void OptionsUINextButton_Click(object sender, RoutedEventArgs e) => Finish(true);
@@ -32,12 +32,17 @@ namespace ZuneModdingHelper
Close();
}
- public AbstractUICollectionViewModel ViewModel
+ public ModViewModel ViewModel
{
- get => (AbstractUICollectionViewModel)GetValue(ViewModelProperty);
+ get => (ModViewModel)GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(
- nameof(ViewModel), typeof(AbstractUICollectionViewModel), typeof(AbstractUIGroupDialog));
+ nameof(ViewModel), typeof(ModViewModel), typeof(OptionsUIDialog));
+
+ private async void Window_Loaded(object sender, RoutedEventArgs e)
+ {
+ await ViewModel.LoadAsync();
+ }
}
}
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 @@
-
+