diff --git a/pkNX.Containers/VFS/FileSystems/PhysicalFileSystem.cs b/pkNX.Containers/VFS/FileSystems/PhysicalFileSystem.cs index de8da14c..b4e59146 100644 --- a/pkNX.Containers/VFS/FileSystems/PhysicalFileSystem.cs +++ b/pkNX.Containers/VFS/FileSystems/PhysicalFileSystem.cs @@ -15,6 +15,9 @@ public PhysicalFileSystem(string physicalRoot) if (!physicalRoot.EndsWith(Path.DirectorySeparatorChar)) physicalRoot += Path.DirectorySeparatorChar; PhysicalRoot = physicalRoot; + + if (!Directory.Exists(PhysicalRoot)) + throw new ArgumentException("The specified path does not exist.", nameof(physicalRoot)); } public string GetPhysicalPath(FileSystemPath path) diff --git a/pkNX.Containers/VFS/Util/FileSystemPath.cs b/pkNX.Containers/VFS/Util/FileSystemPath.cs index ed0c5d5e..6d4f9b95 100644 --- a/pkNX.Containers/VFS/Util/FileSystemPath.cs +++ b/pkNX.Containers/VFS/Util/FileSystemPath.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; +using System.Xml.Linq; namespace pkNX.Containers.VFS; @@ -59,7 +60,7 @@ private FileSystemPath(string path) return Parse(path); } - public static implicit operator string(FileSystemPath path) + public static explicit operator string(FileSystemPath path) { return path.ToString(); } @@ -80,6 +81,21 @@ public static FileSystemPath Parse(string s) return new(s); } + public static FileSystemPath operator +(FileSystemPath path, string strPathToAppend) + { + return path.AppendPath(strPathToAppend); + } + + public static FileSystemPath operator +(FileSystemPath path, FileSystemPath pathToAppend) + { + return path.AppendPath(pathToAppend); + } + + public static string operator +(string path, FileSystemPath pathToAppend) + { + return path + (string)pathToAppend; + } + [Pure] public FileSystemPath AppendPath(string strPath) { @@ -159,9 +175,29 @@ public string GetExtension() { if (!IsFile) throw new ArgumentException("The specified FileSystemPath is not a file."); - string name = EntityName; - int extensionIndex = name.LastIndexOf('.'); - return extensionIndex <= 0 ? string.Empty : name[extensionIndex..]; + + int lastPeriod = EntityName.LastIndexOf('.'); + return EntityName[(lastPeriod + 1)..]; + } + + [Pure] + public string GetFileNameWithoutExtension() + { + if (!IsFile) + throw new ArgumentException("The specified FileSystemPath is not a file."); + + int lastPeriod = EntityName.LastIndexOf('.'); + return EntityName[..lastPeriod]; + } + + [Pure] + public (string, string) GetFileNameAndExtension() + { + if (!IsFile) + throw new ArgumentException("The specified FileSystemPath is not a file."); + + int lastPeriod = EntityName.LastIndexOf('.'); + return (EntityName[..lastPeriod], EntityName[(lastPeriod + 1)..]); } [Pure] @@ -169,6 +205,7 @@ public FileSystemPath ChangeExtension(string extension) { if (!IsFile) throw new ArgumentException("The specified FileSystemPath is not a file."); + string name = EntityName; int extensionIndex = name.LastIndexOf('.'); if (extensionIndex < 0) diff --git a/pkNX.Containers/VFS/Util/VirtualDirectory.cs b/pkNX.Containers/VFS/Util/VirtualDirectory.cs index 2b31bdd5..90432476 100644 --- a/pkNX.Containers/VFS/Util/VirtualDirectory.cs +++ b/pkNX.Containers/VFS/Util/VirtualDirectory.cs @@ -8,6 +8,8 @@ namespace pkNX.Containers.VFS; public string Name => Path.EntityName; public VirtualDirectory ParentDirectory => Create(FileSystem, Path.ParentPath); + public override string ToString() => Path.ToString(); + public void CopyTo(VirtualDirectory destination) { FileSystem.Copy(Path, destination.FileSystem, destination.Path.AppendDirectory(Name)); diff --git a/pkNX.Containers/VFS/Util/VirtualFile.cs b/pkNX.Containers/VFS/Util/VirtualFile.cs index fd9c0625..2dac7f80 100644 --- a/pkNX.Containers/VFS/Util/VirtualFile.cs +++ b/pkNX.Containers/VFS/Util/VirtualFile.cs @@ -8,23 +8,11 @@ namespace pkNX.Containers.VFS; public string Name => Path.EntityName; public VirtualDirectory ParentDirectory => VirtualDirectory.Create(FileSystem, Path.ParentPath); - public (string, string) GetFileNameAndExtension() - { - int lastPeriod = Name.LastIndexOf('.'); - return (Name[..lastPeriod], Name[(lastPeriod + 1)..]); - } + public override string ToString() => Path.ToString(); - public string GetFileNameWithoutExtension() - { - int lastPeriod = Name.LastIndexOf('.'); - return Name[..lastPeriod]; - } - - public string GetExtension() - { - int lastPeriod = Name.LastIndexOf('.'); - return Name[(lastPeriod + 1)..]; - } + public (string, string) GetFileNameAndExtension() => Path.GetFileNameAndExtension(); + public string GetFileNameWithoutExtension() => Path.GetFileNameWithoutExtension(); + public string GetExtension() => Path.GetExtension(); public Stream Open(FileMode mode = FileMode.Open, FileAccess access = FileAccess.Read) { diff --git a/pkNX.Containers/VFS/VirtualFileSystem.cs b/pkNX.Containers/VFS/VirtualFileSystem.cs index a737c35e..ad1ec8a2 100644 --- a/pkNX.Containers/VFS/VirtualFileSystem.cs +++ b/pkNX.Containers/VFS/VirtualFileSystem.cs @@ -1,53 +1,56 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.IO; using System.Linq; using System.Runtime.CompilerServices; namespace pkNX.Containers.VFS; -public record MountPoint +public record MountPoint : IComparable { public FileSystemPath MountPath { get; } public IFileSystem FileSystem { get; } - public FileSystemPath AddMountPoint(FileSystemPath path) - { - return MountPath.AppendPath(path); - } - - public FileSystemPath RemoveMountPoint(FileSystemPath path) - { - return path.IsRoot ? path : path.MakeRelativeTo(MountPath); - } - public MountPoint(FileSystemPath mountPath, IFileSystem fileSystem) { MountPath = mountPath; - FileSystem = fileSystem.AsRelativeFileSystem(RemoveMountPoint, AddMountPoint); + FileSystem = fileSystem.AsRelativeFileSystem( + path => path.IsRoot ? path : path.MakeRelativeTo(MountPath), + path => MountPath.AppendPath(path) + ); + } + + public int CompareTo(MountPoint? other) + { + if (other == null) + return 1; + + return MountPath.CompareTo(other.MountPath); } } public class VirtualFileSystem : IFileSystem { public static VirtualFileSystem Current { get; private set; } = null!; + public bool IsReadOnly => _mounts.All(x => x.FileSystem.IsReadOnly); - public SortedSet Mounts { get; } - public bool IsReadOnly => Mounts.All(x => x.FileSystem.IsReadOnly); + private readonly SortedSet _mounts; - public VirtualFileSystem(IEnumerable mounts) + public VirtualFileSystem(IEnumerable mounts) : + this(mounts.ToArray()) { - Mounts = new SortedSet(mounts); + } + + public VirtualFileSystem(params MountPoint[] mounts) + { + _mounts = new(mounts); Current = this; } - public VirtualFileSystem(params MountPoint[] mounts) : - this(mounts.AsEnumerable()) - { } - public void Dispose() { - foreach (var fs in Mounts.Select(x => x.FileSystem)) + foreach (var fs in _mounts.Select(x => x.FileSystem)) fs.Dispose(); GC.SuppressFinalize(this); @@ -55,7 +58,22 @@ public void Dispose() protected MountPoint GetMountPoint(FileSystemPath path) { - return Mounts.First(mount => mount.MountPath == path || mount.MountPath.IsParentOf(path)); + return _mounts.First(mount => mount.MountPath == path || mount.MountPath.IsParentOf(path)); + } + + public bool IsMounted(FileSystemPath mountPath) + { + return _mounts.Any(mount => mount.MountPath == mountPath || mount.MountPath.IsParentOf(mountPath)); + } + + public void Mount(MountPoint mountPoint) + { + _mounts.Add(mountPoint); + } + + public void UnMount(MountPoint mountPoint) + { + _mounts.Remove(mountPoint); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/pkNX.Game/File/GamePath.cs b/pkNX.Game/File/GamePath.cs index ec9eba16..2791e698 100644 --- a/pkNX.Game/File/GamePath.cs +++ b/pkNX.Game/File/GamePath.cs @@ -11,9 +11,16 @@ namespace pkNX.Game; public static class GamePath { + public static readonly GameVersion[] SupportedGames = { GameVersion.SW, GameVersion.SH, GameVersion.PLA, GameVersion.SL, GameVersion.VL }; + private static GameVersion CurrentGame { get; set; } private static int CurrentLanguage { get; set; } + public static void InitializeWorkspace() + { + + } + public static void Initialize(GameVersion game, int language) { CurrentGame = game; @@ -30,6 +37,11 @@ public static FileSystemPath GetDirectoryPath(GameFile file, int language) return GetDirectoryPath(file, CurrentGame, language); } + public static FileSystemPath GetDirectoryPath(GameFile file, GameVersion game) + { + return GetDirectoryPath(file, game, CurrentLanguage); + } + public static FileSystemPath GetDirectoryPath(GameFile file, GameVersion game, int language) { if (file is GameFile.GameText or GameFile.StoryText) @@ -38,8 +50,12 @@ public static FileSystemPath GetDirectoryPath(GameFile file, GameVersion game, i return game switch { GameVersion.GG => GetDirectoryPath_GG(file), + GameVersion.SW => GetDirectoryPath_SWSH(file), + GameVersion.SH => GetDirectoryPath_SWSH(file), GameVersion.SWSH => GetDirectoryPath_SWSH(file), GameVersion.PLA => GetDirectoryPath_PLA(file), + GameVersion.SL => GetDirectoryPath_SV(file), + GameVersion.VL => GetDirectoryPath_SV(file), GameVersion.SV => GetDirectoryPath_SV(file), _ => throw new NotSupportedException($"The selected game ({game}) is currently not mapped") @@ -89,6 +105,8 @@ private static FileSystemPath GetDirectoryPath_SWSH(GameFile file) GameFile.StoryText8 => "/romfs/bin/message/Simp_Chinese/script/", GameFile.StoryText9 => "/romfs/bin/message/Trad_Chinese/script/", + GameFile.PokemonArchiveFolder => "/romfs/bin/archive/pokemon/", + _ => throw new NotSupportedException($"The selected file ({file}) is currently not mapped") }; diff --git a/pkNX.Game/GameManagerPLA.cs b/pkNX.Game/GameManagerPLA.cs index c1b8d5d7..0dae4977 100644 --- a/pkNX.Game/GameManagerPLA.cs +++ b/pkNX.Game/GameManagerPLA.cs @@ -21,8 +21,6 @@ public class GameManagerPLA : GameManager /// public GameData8a Data { get; protected set; } = null!; - public VirtualFileSystem VFS { get; private set; } = null!; - protected override void SetMitm() { var basePath = Path.GetDirectoryName(ROM.RomFS); @@ -32,12 +30,6 @@ protected override void SetMitm() var tid = ROM.ExeFS != null ? TitleID : "arceus"; var redirect = Path.Combine(basePath, tid); FileMitm.SetRedirect(basePath, redirect); - - var cleanRomFS = new PhysicalFileSystem(basePath + "/romfs/").AsReadOnlyFileSystem(); - var moddedRomFS = new PhysicalFileSystem(redirect + "/romfs/"); - - var layeredFS = new LayeredFileSystem(moddedRomFS, cleanRomFS); - VFS = new VirtualFileSystem(new MountPoint("/romfs/", layeredFS)); } public override void Initialize() diff --git a/pkNX.Structures/ArrayUtil.cs b/pkNX.Structures/ArrayUtil.cs index 1c424278..7d40c1d6 100644 --- a/pkNX.Structures/ArrayUtil.cs +++ b/pkNX.Structures/ArrayUtil.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.Contracts; using System.Linq; namespace pkNX.Structures; @@ -199,16 +200,16 @@ internal static T[] ConcatAll(T[] arr1, T[] arr2, ReadOnlySpan arr3) public static class ArrayUtilsExt { - public static TSource[] Append(this TSource[] first, TSource second, params TSource[] third) + [Pure] + public static TSource[] Concat(this TSource[] src, params TSource[] toAdd) { - return ArrayUtil.ConcatAll(first, new[] { second }, third); + return ArrayUtil.ConcatAll(src, toAdd); } - public static TSource[] Remove(this TSource[] first, TSource toRemove) + [Pure] + public static TSource[] Remove(this TSource[] src, params TSource[] toRemove) { - var list = first.ToList(); - list.Remove(toRemove); - return list.ToArray(); + return src.Except(toRemove).ToArray(); } } diff --git a/pkNX.Structures/GameVersion.cs b/pkNX.Structures/GameVersion.cs index 2feca0e1..03c53df3 100644 --- a/pkNX.Structures/GameVersion.cs +++ b/pkNX.Structures/GameVersion.cs @@ -1,3 +1,5 @@ +using pkNX.Containers.VFS; + namespace pkNX.Structures; /// @@ -512,3 +514,39 @@ public enum GameVersion Stadium2, #endregion } + + +public static class GameVersionExt +{ + public static string GetTitleID(this GameVersion game) => game switch + { + GameVersion.PLA => "01001F5010DFA000", + GameVersion.SW => "0100ABF008968000", + GameVersion.SH => "01008DB008C2C000", + GameVersion.SL => "0100A3D008C5C000", + GameVersion.VL => "01008F6008C5E000", + _ => throw new System.NotImplementedException(), + }; + + public static string GetTitleName(this GameVersion game) => game switch + { + GameVersion.PLA => "Pokémon Legends Arceus", + GameVersion.SW => "Pokémon Sword", + GameVersion.SH => "Pokémon Shield", + GameVersion.SL => "Pokémon Scarlet", + GameVersion.VL => "Pokémon Violet", + _ => throw new System.NotImplementedException(), + }; + + public static string ToLowerString(this GameVersion game) => game.ToString().ToLowerInvariant(); + public static FileSystemPath ToMountPath(this GameVersion game) => $"/{game.ToString().ToLowerInvariant()}/"; +} + +public static class VFSExt +{ + public static bool IsGameLoaded(this VirtualFileSystem vfs, GameVersion game) + { + var gamePath = game.ToMountPath() + "romfs/"; + return vfs.IsMounted(gamePath); + } +} diff --git a/pkNX.WinForms/Controls/Arceus/ItemDrop.xaml b/pkNX.WinForms/Controls/Arceus/ItemDrop.xaml new file mode 100644 index 00000000..3b40f2da --- /dev/null +++ b/pkNX.WinForms/Controls/Arceus/ItemDrop.xaml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + diff --git a/pkNX.WinForms/Controls/Arceus/ItemDrop.xaml.cs b/pkNX.WinForms/Controls/Arceus/ItemDrop.xaml.cs new file mode 100644 index 00000000..4e4567f3 --- /dev/null +++ b/pkNX.WinForms/Controls/Arceus/ItemDrop.xaml.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace pkNX.WinForms.Controls +{ + /// + /// Interaction logic for ItemDrop.xaml + /// + public partial class ItemDrop : UserControl + { + public ItemDrop() + { + InitializeComponent(); + } + } +} diff --git a/pkNX.WinForms/Controls/EvolutionEntry.xaml b/pkNX.WinForms/Controls/EvolutionEntry.xaml index a8ed0b26..c53c9dc8 100644 --- a/pkNX.WinForms/Controls/EvolutionEntry.xaml +++ b/pkNX.WinForms/Controls/EvolutionEntry.xaml @@ -7,7 +7,7 @@ mc:Ignorable="d" x:Name="Root" d:Background="{StaticResource DockWindow.Background}" - d:DesignHeight="74" d:DesignWidth="692"> + d:DesignWidth="692"> diff --git a/pkNX.WinForms/Controls/EvolutionEntry.xaml.cs b/pkNX.WinForms/Controls/EvolutionEntry.xaml.cs index f6584b74..0d8945e3 100644 --- a/pkNX.WinForms/Controls/EvolutionEntry.xaml.cs +++ b/pkNX.WinForms/Controls/EvolutionEntry.xaml.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Windows; -using static System.Windows.Forms.VisualStyles.VisualStyleElement; namespace pkNX.WinForms; diff --git a/pkNX.WinForms/Controls/LabeledControl.cs b/pkNX.WinForms/Controls/LabeledControl.cs new file mode 100644 index 00000000..f522ce6a --- /dev/null +++ b/pkNX.WinForms/Controls/LabeledControl.cs @@ -0,0 +1,15 @@ +using System; +using System.Windows; +using System.Windows.Controls; + +namespace pkNX.WinForms; + +public class LabeledControl : ContentControl +{ + public static readonly DependencyProperty LabelProperty = DependencyProperty.Register(nameof(Label), typeof(string), typeof(LabeledControl), new PropertyMetadata("Label:")); + public string Label + { + get { return (string)GetValue(LabelProperty); } + set { SetValue(LabelProperty, value); } + } +} diff --git a/pkNX.WinForms/Controls/PathSelector.xaml b/pkNX.WinForms/Controls/PathSelector.xaml new file mode 100644 index 00000000..a5b86f46 --- /dev/null +++ b/pkNX.WinForms/Controls/PathSelector.xaml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/pkNX.WinForms/Controls/PathSelector.xaml.cs b/pkNX.WinForms/Controls/PathSelector.xaml.cs new file mode 100644 index 00000000..0b9e649f --- /dev/null +++ b/pkNX.WinForms/Controls/PathSelector.xaml.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace pkNX.WinForms +{ + /// + /// Interaction logic for PathSelector.xaml + /// + public partial class PathSelector : UserControl + { + public static readonly DependencyProperty PathProperty = DependencyProperty.Register(nameof(Path), typeof(string), typeof(PathSelector), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); + public static readonly DependencyProperty IsPathReadOnlyProperty = DependencyProperty.Register(nameof(IsPathReadOnly), typeof(bool), typeof(PathSelector), new PropertyMetadata(false)); + public static readonly DependencyProperty FolderSelectDescriptionProperty = DependencyProperty.Register(nameof(FolderSelectDescription), typeof(string), typeof(PathSelector), new PropertyMetadata(null)); + + public string Path + { + get { return (string)GetValue(PathProperty); } + set { SetValue(PathProperty, value); } + } + + public bool IsPathReadOnly + { + get { return (bool)GetValue(IsPathReadOnlyProperty); } + set { SetValue(IsPathReadOnlyProperty, value); } + } + + public string FolderSelectDescription + { + get { return (string)GetValue(FolderSelectDescriptionProperty); } + set { SetValue(FolderSelectDescriptionProperty, value); } + } + + public event TextChangedEventHandler? PathChanged; + + public PathSelector() + { + InitializeComponent(); + } + + private void B_PathSelect_Click(object sender, RoutedEventArgs e) + { + using System.Windows.Forms.FolderBrowserDialog fbd = new(); + fbd.Description = FolderSelectDescription; + fbd.UseDescriptionForTitle = true; + fbd.ShowNewFolderButton = false; + + if (fbd.ShowDialog() == System.Windows.Forms.DialogResult.OK) + Path = fbd.SelectedPath; + } + + private void TextBox_TextChanged(object sender, TextChangedEventArgs e) + { + PathChanged?.Invoke(sender, e); + } + } +} diff --git a/pkNX.WinForms/MainEditor/EditorPLA.cs b/pkNX.WinForms/MainEditor/EditorPLA.cs index a83ee86f..9b47a05e 100644 --- a/pkNX.WinForms/MainEditor/EditorPLA.cs +++ b/pkNX.WinForms/MainEditor/EditorPLA.cs @@ -749,8 +749,8 @@ public void EditPokedexRankTable() [EditorCallable()] public void ModelConverter() { - using var form = new ModelConverter(ROM); - form.ShowDialog(); + var form = new ModelEditor(); + form.Show(); } [EditorCallable()] diff --git a/pkNX.WinForms/MainEditor/EditorProvider.cs b/pkNX.WinForms/MainEditor/EditorProvider.cs index 2cd732ba..df9e35ad 100644 --- a/pkNX.WinForms/MainEditor/EditorProvider.cs +++ b/pkNX.WinForms/MainEditor/EditorProvider.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; using System.Runtime.Intrinsics.Arm; using System.Windows.Controls; +using pkNX.Containers.VFS; using pkNX.Game; using pkNX.Structures; @@ -16,6 +18,7 @@ public abstract class EditorBase public GameVersion Game => ROM.Game; public int Language { get => ROM.Language; set => ROM.Language = value; } public string? Location { get; private set; } + public VirtualFileSystem VFS { get; private set; } = null!; private const string prefix = "Edit"; private readonly MethodInfo[] editorMethods; @@ -36,11 +39,55 @@ protected EditorBase() public void Initialize() { + SetupVFS(); GamePath.Initialize(ROM.Game, ROM.Language); ROM.Initialize(); UIStaticSources.SetupForGame(ROM); } + public void SetupVFS() + { + var settings = ProgramSettings.LoadSettings(); + + var mounts = new List(); + + foreach (var game in settings.GameConfigs) + { + if (!game.IsValid) + continue; + + // Map romfs to the game mount path (e.g. /pla/romfs/game/files/) + string exportPath = Path.Combine(game.ExportPath, "romfs"); + if (!Directory.Exists(exportPath)) + Directory.CreateDirectory(exportPath); + + if (!Directory.Exists(game.RomFSPath)) + throw new DirectoryNotFoundException($"The specified romfs path for {game.Game} does not exist."); + + var cleanRomFS = new PhysicalFileSystem(game.RomFSPath).AsReadOnlyFileSystem(); + var moddedRomFS = new PhysicalFileSystem(exportPath); + var layeredRomFS = new LayeredFileSystem(moddedRomFS, cleanRomFS); + + mounts.Add(new MountPoint($"/{game.Game.ToLowerString()}/romfs/", layeredRomFS)); + // Map romfs (without game title) only to the currently selected game + if (game.Game == settings.GameOverride) + mounts.Add(new MountPoint("/romfs/", layeredRomFS)); + + if (!Directory.Exists(game.ExeFSPath)) + continue; + + // Currently we don't support modding exefs, so just map the (read-only) clean exefs + var cleanExeFS = new PhysicalFileSystem(game.ExeFSPath).AsReadOnlyFileSystem(); + mounts.Add(new MountPoint($"/{game.Game.ToLowerString()}/exefs/", cleanExeFS)); + + // Map exefs (without game title) only to the currently selected game + if (game.Game == settings.GameOverride) + mounts.Add(new MountPoint("/exefs/", cleanExeFS)); + } + + VFS = new VirtualFileSystem(mounts); + } + public int CountControlsForCategory(EditorCategory category) => editorAttributes.Count(a => a.Category == category); public IEnumerable GetControls(EditorCategory category = EditorCategory.None, bool displayAdvanced = false) diff --git a/pkNX.WinForms/ProgramSettings.cs b/pkNX.WinForms/ProgramSettings.cs index 21b8739d..6f8946a6 100644 --- a/pkNX.WinForms/ProgramSettings.cs +++ b/pkNX.WinForms/ProgramSettings.cs @@ -1,18 +1,141 @@ using pkNX.Structures; using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; namespace pkNX.WinForms; +public class GameConfig : INotifyPropertyChanged +{ + private string? _workspacePath; + + public GameVersion Game { get; init; } = GameVersion.Invalid; + public string? WorkspacePath + { + get => _workspacePath; + set + { + _workspacePath = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(RomFSPath)); + OnPropertyChanged(nameof(ExeFSPath)); + OnPropertyChanged(nameof(ExportPath)); + } + } + public string? RomFSPathOverride { get; set; } + public string? ExeFSPathOverride { get; set; } + public string? ExportPathOverride { get; set; } + + public string GameTitle => Game.GetTitleName(); + public bool IsValid => Game != GameVersion.Invalid && !string.IsNullOrWhiteSpace(WorkspacePath); + public bool OverrideDefaultPaths { get; set; } + + private string GetOverridePathOrDefault(string? overridePath, string defaultFolder) + { + if (string.IsNullOrWhiteSpace(WorkspacePath)) + return string.Empty; + + if (OverrideDefaultPaths && !string.IsNullOrWhiteSpace(overridePath)) + return overridePath; + + return Path.Combine(WorkspacePath, defaultFolder); + } + + public string RomFSPath + { + get => GetOverridePathOrDefault(RomFSPathOverride, "romfs"); + set => RomFSPathOverride = value; + } + public string ExeFSPath + { + get => GetOverridePathOrDefault(ExeFSPathOverride, "exefs"); + set => ExeFSPathOverride = value; + } + public string ExportPath + { + get => GetOverridePathOrDefault(ExportPathOverride, Game.GetTitleID()); + set => ExportPathOverride = value; + } + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} + public class ProgramSettings { public static readonly string ProgramSettingsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "settings.json"); - public static ProgramSettings LoadSettings() => SettingsSerializer.GetSettings(ProgramSettingsPath).Result; + public static ProgramSettings LoadSettings() + { + var settings = SettingsSerializer.GetSettings(ProgramSettingsPath).Result; + settings.UpdateForSupportedGames(); + return settings; + } + public static async Task SaveSettings(ProgramSettings settings) => await SettingsSerializer.SaveSettings(settings, ProgramSettingsPath); public int Language { get; set; } = 2; + public bool DisplayAdvanced { get; set; } = false; + public string GamePath { get; set; } = string.Empty; public GameVersion GameOverride { get; set; } = GameVersion.Any; - public bool DisplayAdvanced { get; set; } = false; + + public GameConfig[] GameConfigs { get; set; } = Array.Empty(); + + public void UpdateForSupportedGames() + { + // Add all missing supported games to the config + if (GameConfigs.Length >= Game.GamePath.SupportedGames.Length) + return; + + var missing = Game.GamePath.SupportedGames.Except(GameConfigs.Select(x => x.Game)); + GameConfigs = GameConfigs + .Concat(missing.Select(x => new GameConfig { Game = x })) + .OrderBy(x => x.Game) + .ToArray(); + + GameConfigs.First(x => x.Game == GameOverride).WorkspacePath = GamePath; + } +} + +public static class SettingsSerializer +{ + public static async Task GetSettings(string path, CancellationToken token = default) where T : new() + { + if (!File.Exists(path)) + return new T(); + + try + { + var data = await File.ReadAllTextAsync(path, token).ConfigureAwait(false); + return System.Text.Json.JsonSerializer.Deserialize(data) ?? new T(); + } + catch (Exception ex) + { + Debug.WriteLine($"Unable to load settings from {path}: {ex.Message}"); + return new T(); + } + } + + public static async Task SaveSettings(T settings, string path, CancellationToken token = default) + { + try + { + var json = System.Text.Json.JsonSerializer.Serialize(settings); + await File.WriteAllTextAsync(path, json, token).ConfigureAwait(false); + } + catch (Exception ex) + { + Debug.WriteLine(ex.Message); + } + } } diff --git a/pkNX.WinForms/SettingsSerializer.cs b/pkNX.WinForms/SettingsSerializer.cs deleted file mode 100644 index 356697d7..00000000 --- a/pkNX.WinForms/SettingsSerializer.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace pkNX.WinForms; - -public static class SettingsSerializer -{ - public static async Task GetSettings(string path, CancellationToken token = default) where T : new() - { - if (!File.Exists(path)) - return new T(); - - try - { - var data = await File.ReadAllTextAsync(path, token).ConfigureAwait(false); - return System.Text.Json.JsonSerializer.Deserialize(data) ?? new T(); - } - catch (Exception ex) - { - Debug.WriteLine($"Unable to load settings from {path}: {ex.Message}"); - return new T(); - } - } - - public static async Task SaveSettings(T settings, string path, CancellationToken token = default) - { - try - { - var json = System.Text.Json.JsonSerializer.Serialize(settings); - await File.WriteAllTextAsync(path, json, token).ConfigureAwait(false); - } - catch (Exception ex) - { - Debug.WriteLine(ex.Message); - } - } -} diff --git a/pkNX.WinForms/Themes/Styles.xaml b/pkNX.WinForms/Themes/Styles.xaml index 0a308678..2f4d316f 100644 --- a/pkNX.WinForms/Themes/Styles.xaml +++ b/pkNX.WinForms/Themes/Styles.xaml @@ -199,6 +199,26 @@ M5,-0 L9,5 1,5 z M0 26 L0 6 Q0 0 3 0 Q6 0 6 6 L6 26 Q6 32 3 32 Q0 32 0 26 z + + + +