Temp (don't push)

This commit is contained in:
duckdoom4
2023-09-15 17:23:31 +02:00
parent 20905d0343
commit 79c59b33ef
30 changed files with 791 additions and 119 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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));

View File

@@ -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)
{

View File

@@ -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<MountPoint>
{
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<MountPoint> Mounts { get; }
public bool IsReadOnly => Mounts.All(x => x.FileSystem.IsReadOnly);
private readonly SortedSet<MountPoint> _mounts;
public VirtualFileSystem(IEnumerable<MountPoint> mounts)
public VirtualFileSystem(IEnumerable<MountPoint> mounts) :
this(mounts.ToArray())
{
Mounts = new SortedSet<MountPoint>(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)]

View File

@@ -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")
};

View File

@@ -21,8 +21,6 @@ public class GameManagerPLA : GameManager
/// </summary>
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()

View File

@@ -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>(T[] arr1, T[] arr2, ReadOnlySpan<T> arr3)
public static class ArrayUtilsExt
{
public static TSource[] Append<TSource>(this TSource[] first, TSource second, params TSource[] third)
[Pure]
public static TSource[] Concat<TSource>(this TSource[] src, params TSource[] toAdd)
{
return ArrayUtil.ConcatAll(first, new[] { second }, third);
return ArrayUtil.ConcatAll(src, toAdd);
}
public static TSource[] Remove<TSource>(this TSource[] first, TSource toRemove)
[Pure]
public static TSource[] Remove<TSource>(this TSource[] src, params TSource[] toRemove)
{
var list = first.ToList();
list.Remove(toRemove);
return list.ToArray();
return src.Except(toRemove).ToArray();
}
}

View File

@@ -1,3 +1,5 @@
using pkNX.Containers.VFS;
namespace pkNX.Structures;
/// <summary>
@@ -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);
}
}

View File

@@ -0,0 +1,32 @@
<UserControl x:Class="pkNX.WinForms.Controls.ItemDrop"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:pkNX.WinForms"
mc:Ignorable="d"
d:DesignWidth="343">
<GroupBox Header="Field Drops:" Margin="3">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="38"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Grid.Column="0" Grid.Row="0" Content="Regular Item:" HorizontalAlignment="Right"/>
<Label Grid.Column="0" Grid.Row="1" Content="Rare Item:" HorizontalAlignment="Right"/>
<ComboBox Grid.Column="1" Grid.Row="0" Margin="3" ItemsSource="{Binding Source={x:Static local:UIStaticSources.ItemsList}}" />
<ComboBox Grid.Column="1" Grid.Row="1" Margin="3" ItemsSource="{Binding Source={x:Static local:UIStaticSources.ItemsList}}" />
<local:NumericTextBox Grid.Column="2" Grid.Row="0" Margin="3" Value="50" MaxValue="100"/>
<local:NumericTextBox Grid.Column="2" Grid.Row="1" Margin="3" Value="50" MaxValue="100"/>
<Label Grid.Column="3" Grid.Row="0" Content="%"/>
<Label Grid.Column="3" Grid.Row="1" Content="%"/>
</Grid>
</GroupBox>
</UserControl>

View File

@@ -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
{
/// <summary>
/// Interaction logic for ItemDrop.xaml
/// </summary>
public partial class ItemDrop : UserControl
{
public ItemDrop()
{
InitializeComponent();
}
}
}

View File

@@ -7,7 +7,7 @@
mc:Ignorable="d"
x:Name="Root"
d:Background="{StaticResource DockWindow.Background}"
d:DesignHeight="74" d:DesignWidth="692">
d:DesignWidth="692">
<UserControl.Resources>
<local:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
</UserControl.Resources>

View File

@@ -8,7 +8,6 @@
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows;
using static System.Windows.Forms.VisualStyles.VisualStyleElement;
namespace pkNX.WinForms;

View File

@@ -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); }
}
}

View File

@@ -0,0 +1,19 @@
<UserControl x:Class="pkNX.WinForms.PathSelector"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:pkNX.WinForms"
x:Name="Root"
mc:Ignorable="d" d:DesignWidth="500">
<Grid Height="27">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding Path, ElementName=Root}" Margin="3" IsReadOnly="{Binding IsPathReadOnly, ElementName=Root}" TextChanged="TextBox_TextChanged" />
<Button Grid.Column="1" Margin="3" Padding="3" Width="32" Click="B_PathSelect_Click">
<local:IconImage Icon="FolderOpen" IconFont="Solid" Height="14" />
</Button>
</Grid>
</UserControl>

View File

@@ -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
{
/// <summary>
/// Interaction logic for PathSelector.xaml
/// </summary>
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);
}
}
}

View File

@@ -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()]

View File

@@ -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<MountPoint>();
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<EditorButtonData> GetControls(EditorCategory category = EditorCategory.None, bool displayAdvanced = false)

View File

@@ -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<ProgramSettings>(ProgramSettingsPath).Result;
public static ProgramSettings LoadSettings()
{
var settings = SettingsSerializer.GetSettings<ProgramSettings>(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<GameConfig>();
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<T> GetSettings<T>(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<T>(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>(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);
}
}
}

View File

@@ -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<T> GetSettings<T>(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<T>(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>(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);
}
}
}

View File

@@ -199,6 +199,26 @@
<Geometry x:Key="ExpanderToggleButtonArrow">M5,-0 L9,5 1,5 z</Geometry>
<Geometry x:Key="SliderThumb.Horizontal.Default">M0 26 L0 6 Q0 0 3 0 Q6 0 6 6 L6 26 Q6 32 3 32 Q0 32 0 26 z</Geometry>
<!-- local:LabeledControl -->
<Style TargetType="{x:Type local:LabeledControl}">
<Setter Property="Label" Value="Label: "/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:LabeledControl}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelGroup"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="{TemplateBinding Label}" HorizontalAlignment="Right" />
<ContentControl Grid.Column="1" Content="{TemplateBinding Content}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!--FontStyle-->
<Style TargetType="{x:Type local:IconImage}">
<Setter Property="ToolTipService.InitialShowDelay" Value="{StaticResource ToolTipInitialShowDelay}"/>

View File

@@ -1,4 +1,4 @@
<Window x:Class="pkNX.WinForms.MainWindow"
<Window x:Class="pkNX.WinForms.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
@@ -45,6 +45,7 @@
</MenuItem>
</MenuItem>
<MenuItem Header="Options">
<MenuItem Header="Edit Workspace Settings..." Click="Menu_EditWorkspaceSettings_Click"/>
<MenuItem Header="Language">
<MenuItem.Icon>
<local:IconImage Icon="Language" IconFont="Solid" />

View File

@@ -347,7 +347,7 @@ private void Menu_SetRNGSeed_Click(object sender, EventArgs e)
private void OpenCommandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
{
using FolderBrowserDialog fbd = new();
using System.Windows.Forms.FolderBrowserDialog fbd = new();
fbd.Description = "Select a folder containing the game files.";
fbd.UseDescriptionForTitle = true;
fbd.ShowNewFolderButton = false;
@@ -386,4 +386,11 @@ private async void Menu_DisplayAdvanced_Click(object sender, RoutedEventArgs e)
// Force reload of editor buttons
LoadEditorButtons();
}
private void Menu_EditWorkspaceSettings_Click(object sender, RoutedEventArgs e)
{
var form = new Settings();
form.ShowDialog();
Editor.SetupVFS();
}
}

View File

@@ -0,0 +1,55 @@
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:pkNX.WinForms"
xmlns:forms="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
xmlns:System="clr-namespace:System;assembly=System.Runtime" x:Class="pkNX.WinForms.ModelEditor"
Background="{StaticResource Window.Background}"
WindowStartupLocation="CenterScreen"
x:Name="Root"
mc:Ignorable="d"
Title="ModelEditor" MinWidth="420" MinHeight="250" Width="560" Height="720">
<Window.Resources>
<local:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
</Window.Resources>
<Grid Margin="3">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<local:LabeledControl Label="Convert Mode:">
<CheckBox x:Name="CHK_ConvertMode" Margin="3" VerticalAlignment="Center" IsChecked="True"/>
</local:LabeledControl>
<local:LabeledControl Grid.Column="1" Label="Output Format:" Visibility="{Binding IsChecked, Converter={StaticResource BoolToVisibilityConverter}, ElementName=CHK_ConvertMode}">
<ComboBox MinWidth="108" Margin="3" Visibility="{Binding IsChecked, Converter={StaticResource BoolToVisibilityConverter}, ElementName=CHK_ConvertMode}"/>
</local:LabeledControl>
<Button Grid.Column="2" Click="B_Convert_Click" Margin="3" Width="80" Content="Convert" />
</Grid>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<local:LabeledControl Label="Source:">
<ComboBox x:Name="CB_GameSource" MinWidth="168" Margin="3" DisplayMemberPath="GameTitle" SelectionChanged="CB_GameSource_SelectionChanged"/>
</local:LabeledControl>
<local:LabeledControl Grid.Column="1" Label="Species:">
<ComboBox x:Name="CB_Species" Margin="3" DisplayMemberPath="Name" SelectionChanged="CB_Species_SelectionChanged"/>
</local:LabeledControl>
</Grid>
<WindowsFormsHost Grid.Row="2" Margin="3">
<forms:PropertyGrid x:Name="PG_Model" HelpVisible="False" ToolbarVisible="False"/>
</WindowsFormsHost>
</Grid>
</Window>

View File

@@ -0,0 +1,83 @@
using pkNX.Containers.VFS;
using System;
using System.Collections.Generic;
using System.IO;
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.Shapes;
using pkNX.Game;
using pkNX.Structures;
using pkNX.Containers;
namespace pkNX.WinForms;
/// <summary>
/// Interaction logic for ModelEditor.xaml
/// </summary>
public partial class ModelEditor : Window
{
public ModelEditor()
{
InitializeComponent();
VirtualFileSystem vfs = VirtualFileSystem.Current;
var settings = ProgramSettings.LoadSettings();
var loadedGames = settings.GameConfigs
.Where(x => x.IsValid && vfs.IsGameLoaded(x.Game))
.ToList();
CB_GameSource.ItemsSource = loadedGames;
CB_GameSource.SelectedIndex = loadedGames.FindIndex(x => x.Game == settings.GameOverride);
}
private void B_Convert_Click(object sender, RoutedEventArgs e)
{
}
private void CB_GameSource_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (CB_GameSource.SelectedItem is not GameConfig config)
return;
VirtualFileSystem vfs = VirtualFileSystem.Current;
FileSystemPath pokemonModelDir = config.Game.ToMountPath() + GamePath.GetDirectoryPath(GameFile.PokemonArchiveFolder, config.Game);
var f = vfs.GetFilesInDirectory(pokemonModelDir);
CB_Species.ItemsSource = vfs.GetFiles(pokemonModelDir, path => path.EntityName != "pokeconfig.gfpak");
}
private void CB_Species_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var file = (VirtualFile)CB_Species.SelectedItem;
switch (((GameConfig)CB_GameSource.SelectedItem).Game)
{
case GameVersion.SW:
case GameVersion.SH:
case GameVersion.SWSH:
break;
case GameVersion.PLA:
LoadFile_PLA(file);
break;
default:
throw new NotImplementedException();
}
}
private void LoadFile_PLA(VirtualFile file)
{
using var reader = new BinaryReader(file.OpenRead());
PG_Model.SelectedObject = new GFPack(reader);
}
}

View File

@@ -6,6 +6,7 @@
xmlns:local="clr-namespace:pkNX.WinForms"
xmlns:forms="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
xmlns:arceus="clr-namespace:pkNX.Structures.FlatBuffers.Arceus;assembly=pkNX.Structures.FlatBuffers.Arceus"
xmlns:controls="clr-namespace:pkNX.WinForms.Controls"
mc:Ignorable="d"
x:Name="PkmEditor"
Background="{StaticResource Window.Background}"
@@ -151,8 +152,8 @@
<Label Grid.Column="0" Grid.Row="5" Content="EXP Group:" HorizontalContentAlignment="Right"/>
<ComboBox Grid.Column="1" Grid.Row="5" x:Name="CB_EXPGroup" Margin="3" Grid.ColumnSpan="2" ItemsSource="{Binding Source={x:Static local:UIStaticSources.EXPGroups}}"/>
<Label Grid.Column="0" Grid.Row="6" Content="Form Name:" HorizontalContentAlignment="Right" Visibility="Hidden"/>
<TextBox Grid.Column="1" Grid.ColumnSpan="2" Grid.Row="6" x:Name="TB_FormName" Margin="3" IsReadOnly="True" Visibility="Hidden"/>
<Label Grid.Column="0" Grid.Row="6" Content="Form Name:" HorizontalContentAlignment="Right" />
<TextBox Grid.Column="1" Grid.ColumnSpan="2" Grid.Row="6" x:Name="TB_FormName" Margin="3" IsReadOnly="True" />
</Grid>
<!-- Status -->
@@ -242,7 +243,7 @@
<!-- Tabs -->
<TabControl x:Name="tabControl1" Grid.Row="2" Margin="3">
<!-- Personal Tab -->
<!-- TODO: The personal tab should contain data that is shared between games and the Details section at the top should contain all game specific data -->
<!-- TODO: Sort out the properties better between the personal tab and the details section at the top. The section at the top should contain all the info you'd want to see/edit when other tabs are open -->
<TabItem Header="Personal" >
<Grid Margin="3">
<Grid.RowDefinitions>
@@ -519,7 +520,7 @@
<!-- Level Up -->
<GroupBox Grid.Column="0" Header="Level Up Learnset:">
<DataGrid x:Name="DG_LevelUp" AutoGenerateColumns="False">
<DataGrid x:Name="DG_LevelUp" AutoGenerateColumns="False" UseLayoutRounding="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Level" MinWidth="32" Binding="{Binding Level}"/>
<DataGridTextColumn Header="Mastery Level" MinWidth="32" Binding="{Binding LevelMaster}"/>
@@ -598,8 +599,34 @@
</TabItem>
<!-- Drops Tab -->
<TabItem Header="Drops">
<Grid Margin="3">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<controls:ItemDrop/>
<controls:ItemDrop Grid.Column="1"/>
</Grid>
<DataGrid x:Name="DG_Drops" Grid.Row="1" Margin="0,22,0,0"/>
</Grid>
</TabItem>
<!-- Enhancements Tab -->
<TabItem Header="Enhancements" Visibility="Hidden">
<TabItem Header="Randomizer" Visibility="Hidden">
<TabControl>
<!-- Enhancement sections -->

View File

@@ -31,14 +31,6 @@ public partial class PokemonEditor
public static readonly DependencyProperty SelectedSpeciesProperty = DependencyProperty.Register(nameof(SelectedSpecies), typeof(ushort), typeof(PokemonEditor), new PropertyMetadata((ushort)0, OnSelectedSpeciesChanged));
private static void OnSelectedSpeciesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var self = (PokemonEditor)d;
if (self.Loaded)
self.SaveCurrent();
self.LoadSelectedSpecies();
}
private readonly bool Loaded;
private readonly GameData8a Data;
@@ -83,7 +75,7 @@ private void LoadSelectedSpecies()
var pt = Data.PersonalData;
var index = pt.GetFormIndex(SelectedSpecies, SelectedForm);
//TB_FormName.Text = UIStaticSources.FormsList[index];
TB_FormName.Text = UIStaticSources.FormsList[index];
LoadPersonal((IPersonalInfoPLA)Data.PersonalData[index]);
LoadMisc(Editor.PokeMisc.Root.GetEntry(SelectedSpecies, SelectedForm));
@@ -294,6 +286,8 @@ private void LoadMisc(PokeMisc pokeMisc8a)
pokeMisc8a.DropTable = Editor.FieldDropTables.Table.FirstOrDefault(drops => drops.Hash == pokeMisc8a.DropTableRef);
pokeMisc8a.AlphaDropTable = Editor.FieldDropTables.Table.FirstOrDefault(drops => drops.Hash == pokeMisc8a.AlphaDropTableRef);
DG_Drops.ItemsSource = Editor.FieldDropTables.Table;
}
private void LoadDexResearch(PokedexResearchTask[] pokedexResearchTask)
@@ -430,6 +424,14 @@ private void AutoFillEvolutions()
}
}
private static void OnSelectedSpeciesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var self = (PokemonEditor)d;
if (self.Loaded)
self.SaveCurrent();
self.LoadSelectedSpecies();
}
private void B_DumpTable_Click(object sender, RoutedEventArgs e)
{
var pt = Editor.Personal;

View File

@@ -0,0 +1,55 @@
<Window x:Class="pkNX.WinForms.Settings"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:pkNX.WinForms"
mc:Ignorable="d"
x:Name="Root"
WindowStartupLocation="CenterScreen"
Background="{StaticResource Window.Background}"
Title="Settings" Height="450" Width="800" Closing="Window_Closing">
<Window.Resources>
<ResourceDictionary>
<local:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
</ResourceDictionary>
</Window.Resources>
<Grid>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<GroupBox Header="Game File Paths">
<ItemsControl ItemsSource="{Binding Config.GameConfigs, ElementName=Root}" d:ItemsSource="{d:SampleData ItemCount=3}" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Margin="3"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="local:GameConfig">
<Expander Header="{Binding GameTitle}" Grid.IsSharedSizeScope="True" IsExpanded="{Binding IsValid, Mode=OneTime}">
<StackPanel Margin="14 3 3 3">
<local:LabeledControl Label="Workspace Path:">
<local:PathSelector Path="{Binding WorkspacePath}"/>
</local:LabeledControl>
<local:LabeledControl Label="Override Paths:">
<CheckBox x:Name="CHK_Split" VerticalAlignment="Center" Margin="3" IsChecked="{Binding OverrideDefaultPaths}"/>
</local:LabeledControl>
<StackPanel Visibility="{Binding IsChecked, ElementName=CHK_Split, Converter={StaticResource BoolToVisibilityConverter}}">
<local:LabeledControl Label="RomFS:">
<local:PathSelector Path="{Binding RomFSPath }" IsEnabled="{Binding IsChecked, ElementName=CHK_Split}"/>
</local:LabeledControl>
<local:LabeledControl Label="ExeFS:">
<local:PathSelector Path="{Binding ExeFSPath}" IsEnabled="{Binding IsChecked, ElementName=CHK_Split}"/>
</local:LabeledControl>
<local:LabeledControl Label="Mod Export:">
<local:PathSelector Path="{Binding ExportPath}" IsEnabled="{Binding IsChecked, ElementName=CHK_Split}"/>
</local:LabeledControl>
</StackPanel>
</StackPanel>
</Expander>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</GroupBox>
</ScrollViewer>
</Grid>
</Window>

View File

@@ -0,0 +1,35 @@
using pkNX.Structures;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using pkNX.Game;
namespace pkNX.WinForms;
public class SettingsModel
{
}
/// <summary>
/// Interaction logic for Settings.xaml
/// </summary>
public partial class Settings
{
public ProgramSettings Config { get; set; }
public Settings()
{
Config = ProgramSettings.LoadSettings();
InitializeComponent();
}
private async void Window_Closing(object sender, CancelEventArgs e)
{
// TODO: Verify workspace paths for each game
await ProgramSettings.SaveSettings(Config);
}
}

View File

@@ -409,7 +409,6 @@ private void BatchExport()
return;
}
var result = WinFormsUtil.Prompt(MessageBoxButton.YesNo, "Would you like to unescape newline characters?");
bool newline = result == MessageBoxResult.Yes;