diff --git a/Interface/Actions/ConvertFromXnbAction.cs b/Interface/Actions/ConvertFromXnbAction.cs index 4bbd503..a17938d 100644 --- a/Interface/Actions/ConvertFromXnbAction.cs +++ b/Interface/Actions/ConvertFromXnbAction.cs @@ -1,78 +1,90 @@ - +using System.CommandLine; + using FEZRepacker.Core.Conversion; -using FEZRepacker.Core.FileSystem; -using FEZRepacker.Core.XNB; + +using static FEZRepacker.Interface.CommandLineOptions; namespace FEZRepacker.Interface.Actions { - internal class ConvertFromXnbAction : CommandLineAction + internal class ConvertFromXnbAction : ICommandLineAction { - private const string XnbInput = "xnb-input"; - - private const string FileOutput = "file-output"; - - private const string UseLegacyAo = "use-legacy-ao"; - - private const string UseLegacyTs = "use-legacy-ts"; - public string Name => "--convert-from-xnb"; - public string[] Aliases => new[] { "-x" }; + public string[] Aliases => ["-x"]; public string Description => - "Attempts to convert given XNB input (this can be a path to a single asset or an entire directory) " + - "and save it at given output directory. If input is a directory, dumps all converted files in specified " + - "path recursively. If output directory is not given, outputs next to the input file(s)."; - - public CommandLineArgument[] Arguments => new[] { - new CommandLineArgument(XnbInput), - new CommandLineArgument(FileOutput, ArgumentType.OptionalPositional), - new CommandLineArgument(UseLegacyAo, ArgumentType.Flag), - new CommandLineArgument(UseLegacyTs, ArgumentType.Flag) + "Attempts to convert given XNB input and save it at given output directory"; + + public Argument[] Arguments => [_inputSource, _outputDirectory]; + + public Option[] Options => [UseTrileSetLegacyBundles, UseArtObjectLegacyBundles]; + + private readonly Argument _inputSource = new("input-source") + { + Description = "Given XNB input to convert (this can be a path to a single file or an entire directory).\n" + + " If input is a directory, dumps all converted files in specified path recursively." + }; + + private readonly Argument _outputDirectory = new("output-directory") + { + Arity = ArgumentArity.ZeroOrOne, + Description = "Output directory for saving converted file(s).\n" + + " If output directory is not given, outputs next to the input file(s)." }; - private List FindXnbFilesAtPath(string path) + private static List FindXnbFilesAtPath(FileSystemInfo fileSystem) { - if (Directory.Exists(path)) + if (fileSystem is DirectoryInfo { Exists: true } directoryInfo) { - var xnbFiles = Directory.GetFiles(path, "*.xnb", SearchOption.AllDirectories).ToList(); - Console.WriteLine($"Found {xnbFiles.Count()} XNB files in given directory."); - return xnbFiles; + var xnbFiles = directoryInfo.GetFiles("*.xnb", SearchOption.AllDirectories); + Console.WriteLine($"Found {xnbFiles.Length} XNB files in given directory."); + return xnbFiles.Select(f => f.FullName).ToList(); } - else if (File.Exists(path)) + + if (fileSystem is not FileInfo fileInfo) { - if (Path.GetExtension(path) != ".xnb") - { - throw new Exception("An input file must be an .XNB file."); - } - return new List { path }; + return [fileSystem.FullName]; } - else + + if (!fileInfo.Exists) { throw new FileNotFoundException("Specified input path does not lead to any file or a directory"); } + + return fileInfo.Extension != ".xnb" + ? throw new Exception("An input file must be an .XNB file.") + : [fileSystem.FullName]; } - public void Execute(Dictionary args) + public void Execute(ParseResult result) { - var inputPath = args[XnbInput]; - var outputPath = args.GetValueOrDefault(FileOutput, inputPath); + var inputSource = result.GetRequiredValue(_inputSource); + var outputDirectory = result.GetValue(_outputDirectory); - if (File.Exists(outputPath)) + var outputPath = inputSource switch { - outputPath = Path.GetDirectoryName(outputPath) ?? ""; - } - Directory.CreateDirectory(outputPath); + FileInfo inputFile => inputFile.DirectoryName!, + DirectoryInfo inputDirectory => inputDirectory.FullName, + _ => throw new ArgumentException(nameof(inputSource)) + }; - var xnbFilesToConvert = FindXnbFilesAtPath(inputPath); + if (outputDirectory != null) + { + if (!outputDirectory.Exists) + { + outputDirectory.Create(); + } + outputPath = outputDirectory.FullName; + } - Console.WriteLine($"Converting {xnbFilesToConvert.Count()} XNB files..."); + var xnbFilesToConvert = FindXnbFilesAtPath(inputSource); + Console.WriteLine($"Converting {xnbFilesToConvert.Count} XNB files..."); var filesDone = 0; var settings = new FormatConverterSettings { - UseLegacyArtObjectBundle = args.ContainsKey(UseLegacyAo), - UseLegacyTrileSetBundle = args.ContainsKey(UseLegacyTs) + UseLegacyArtObjectBundle = result.GetValue(UseArtObjectLegacyBundles), + UseLegacyTrileSetBundle = result.GetValue(UseTrileSetLegacyBundles) }; foreach (var xnbPath in xnbFilesToConvert) @@ -83,19 +95,18 @@ public void Execute(Dictionary args) using var outputBundle = UnpackAction.UnpackFile(".xnb", xnbStream, UnpackAction.UnpackingMode.Converted, settings); - var relativePathRaw = xnbPath == inputPath + var relativePathRaw = xnbPath == inputSource.FullName ? Path.GetFileName(xnbPath) - : Path.GetRelativePath(inputPath, xnbPath); + : Path.GetRelativePath(inputSource.FullName, xnbPath); var relativePath = relativePathRaw .Replace("/", "\\") .Replace(".xnb", "", StringComparison.InvariantCultureIgnoreCase); outputBundle.BundlePath = Path.Combine(outputPath, relativePath + outputBundle.MainExtension); - var outputDirectory = Path.GetDirectoryName(outputBundle.BundlePath) ?? ""; - + var outputDirectoryPath = Path.GetDirectoryName(outputBundle.BundlePath) ?? ""; - Directory.CreateDirectory(outputDirectory); + Directory.CreateDirectory(outputDirectoryPath); foreach (var outputFile in outputBundle.Files) { using var fileOutputStream = File.Open(outputBundle.BundlePath + outputFile.Extension, FileMode.Create); diff --git a/Interface/Actions/ConvertToXnbAction.cs b/Interface/Actions/ConvertToXnbAction.cs index bba0433..bca98ea 100644 --- a/Interface/Actions/ConvertToXnbAction.cs +++ b/Interface/Actions/ConvertToXnbAction.cs @@ -1,5 +1,4 @@ - -using System.IO; +using System.CommandLine; using FEZRepacker.Core.Conversion; using FEZRepacker.Core.FileSystem; @@ -7,35 +6,36 @@ namespace FEZRepacker.Interface.Actions { - internal class ConvertToXnbAction : CommandLineAction + internal class ConvertToXnbAction : ICommandLineAction { - private const string FileInput = "file-input"; - - private const string XnbOutput = "xnb-output"; - - private const string UseLegacyAo = "use-legacy-ao"; - - private const string UseLegacyTs = "use-legacy-ts"; - public string Name => "--convert-to-xnb"; - public string[] Aliases => new[] { "-X" }; + public string[] Aliases => ["-X"]; public string Description => - "Attempts to convert given input (this can be a path to a single file or an entire directory) " + - "into XNB file(s) and save it at given output directory. If input is a directory, dumps all converted files in" + - "specified path recursively. If output directory is not given, outputs next to the input file(s)."; + "Attempts to convert given input into XNB file(s) and save it at given output directory"; + + public Argument[] Arguments => [_inputSource]; - public CommandLineArgument[] Arguments => new[] { - new CommandLineArgument(FileInput), - new CommandLineArgument(XnbOutput, ArgumentType.OptionalPositional) + public Option[] Options => [_outputDirectory]; + + private readonly Argument _inputSource = new("input-source") + { + Description = "Given input to convert (this can be a path to a single file or an entire directory).\n" + + " If input is a directory, dumps all converted files in specified path recursively." + }; + + private readonly Option _outputDirectory = new("output-directory") + { + Description = "Output directory for saving deconverted XNB file(s).\n" + + " If output directory is not given, outputs next to the input file(s)." }; public delegate void ConversionFunc(string path, string extension, Stream stream, bool converted); - + public static void PerformBatchConversion(List fileBundles, ConversionFunc processFileFunc) { - Console.WriteLine($"Converting {fileBundles.Count()} assets..."); + Console.WriteLine($"Converting {fileBundles.Count} assets..."); var filesDone = 0; foreach (var fileBundle in fileBundles) { @@ -65,24 +65,34 @@ public static void PerformBatchConversion(List fileBundles, Conversi } } - public void Execute(Dictionary args) + public void Execute(ParseResult result) { - var inputPath = args[FileInput]; - var outputPath = args.GetValueOrDefault(XnbOutput, inputPath); + var inputSource = result.GetRequiredValue(_inputSource); + var outputDirectory = result.GetValue(_outputDirectory); + + var outputPath = inputSource switch + { + FileInfo inputFile => inputFile.DirectoryName!, + DirectoryInfo inputDirectory => inputDirectory.FullName, + _ => throw new ArgumentException(nameof(inputSource)) + }; - if (File.Exists(outputPath)) + if (outputDirectory != null) { - outputPath = Path.GetDirectoryName(outputPath) ?? ""; + if (!outputDirectory.Exists) + { + outputDirectory.Create(); + } + outputPath = outputDirectory.FullName; } - Directory.CreateDirectory(outputPath); - var fileBundles = FileBundle.BundleFilesAtPath(inputPath); - Console.WriteLine($"Found {fileBundles.Count()} file bundles."); + var fileBundles = FileBundle.BundleFilesAtPath(inputSource.FullName); + Console.WriteLine($"Found {fileBundles.Count} file bundles."); PerformBatchConversion(fileBundles, (path, extension, stream, converted) => { if (!converted) return; - + var assetOutputFullPath = Path.Combine(outputPath, $"{path}{extension}"); Directory.CreateDirectory(Path.GetDirectoryName(assetOutputFullPath) ?? ""); diff --git a/Interface/Actions/HelpAction.cs b/Interface/Actions/HelpAction.cs deleted file mode 100644 index a7b06cb..0000000 --- a/Interface/Actions/HelpAction.cs +++ /dev/null @@ -1,63 +0,0 @@ -namespace FEZRepacker.Interface.Actions -{ - internal class HelpAction : CommandLineAction - { - private const string Command = "command"; - - public string Name => "--help"; - public string[] Aliases => new[] { "help", "?", "-?", "-h" }; - public string Description => "Displays help for all commands or help for given command."; - - public CommandLineArgument[] Arguments => new[] { - new CommandLineArgument(Command, ArgumentType.OptionalPositional) - }; - - public void Execute(Dictionary args) - { - if (args.Count == 0) - { - foreach (var cmd in CommandLineInterface.Commands) - { - ShowHelpFor(cmd); - Console.WriteLine(); - } - return; - } - - var arg = args.GetValueOrDefault(Command, string.Empty); - var command = CommandLineInterface.FindCommand(arg); - if (command != null) - { - ShowHelpFor(command); - } - else - { - Console.WriteLine($"Unknown command \"{arg}\"."); - Console.WriteLine($"Use \"--help\" parameter for a list of commands."); - } - } - - private static void ShowHelpFor(CommandLineAction command) - { - string allNames = command.Name; - if (command.Aliases.Length > 0) - { - allNames = $"[{command.Name}, {String.Join(", ", command.Aliases)}]"; - } - - var args = command.Arguments.Select(arg => - { - return arg.Type switch - { - ArgumentType.OptionalPositional => $"<{arg.Name}>", - ArgumentType.RequiredPositional => $"[{arg.Name}]", - ArgumentType.Flag => $"--{arg.Name}", - _ => throw new ArgumentOutOfRangeException($"Invalid argument type: {arg.Type}") - }; - }); - - Console.WriteLine($"Usage: {allNames} {String.Join(" ", args)}"); - Console.WriteLine($"Description: {command.Description}"); - } - } -} diff --git a/Interface/Actions/ListPackageContentAction.cs b/Interface/Actions/ListPackageContentAction.cs index fbf3a5c..eeafcc3 100644 --- a/Interface/Actions/ListPackageContentAction.cs +++ b/Interface/Actions/ListPackageContentAction.cs @@ -1,24 +1,32 @@ -using FEZRepacker.Core.FileSystem; +using System.CommandLine; + +using FEZRepacker.Core.FileSystem; namespace FEZRepacker.Interface.Actions { - internal class ListPackageContentAction : CommandLineAction + internal class ListPackageContentAction : ICommandLineAction { - private const string PakPath = "pak-path"; - public string Name => "--list"; - public string[] Aliases => new[] { "-l" }; - public string Description => "Lists all files contained withing given .PAK package."; - public CommandLineArgument[] Arguments => new[] { - new CommandLineArgument(PakPath) + + public string[] Aliases => ["-l"]; + + public string Description => "Lists all files contained withing given .PAK package"; + + public Argument[] Arguments => [_pakFile]; + + public Option[] Options => []; + + private readonly Argument _pakFile = new("pak-file") + { + Description = "The PAK file to use for listing files." }; - public void Execute(Dictionary args) + public void Execute(ParseResult result) { - var pakPath = args[PakPath]; - var pakPackage = PakPackage.ReadFromFile(pakPath); + var pakFile = result.GetRequiredValue(_pakFile); + var pakPackage = PakPackage.ReadFromFile(pakFile.FullName); - Console.WriteLine($"PAK package \"{pakPath}\" with {pakPackage.Entries.Count} files."); + Console.WriteLine($"PAK package \"{pakFile}\" with {pakPackage.Entries.Count} files."); Console.WriteLine(); foreach (var entry in pakPackage.Entries) diff --git a/Interface/Actions/PackAction.cs b/Interface/Actions/PackAction.cs index 48e3450..14b520c 100644 --- a/Interface/Actions/PackAction.cs +++ b/Interface/Actions/PackAction.cs @@ -1,59 +1,73 @@ -using FEZRepacker.Core.Conversion; +using System.CommandLine; + +using FEZRepacker.Core.Conversion; using FEZRepacker.Core.FileSystem; -using FEZRepacker.Core.XNB; namespace FEZRepacker.Interface.Actions { - internal class PackAction : CommandLineAction + internal class PackAction : ICommandLineAction { - private const string InputDirectoryPath = "input-directory-path"; - private const string DestinationPakPath = "destination-pak-path"; - private const string IncludePakPath = "include-pak-path"; - public string Name => "--pack"; - public string[] Aliases => new[] { "-p" }; + + public string[] Aliases => ["-p"]; + public string Description => - "Loads files from given input directory path, tries to deconvert them and pack into a destination " + - ".PAK file with given path. If include .PAK path is provided, it'll add its content into the new .PAK package."; - public CommandLineArgument[] Arguments => new[] { - new CommandLineArgument(InputDirectoryPath), - new CommandLineArgument(DestinationPakPath), - new CommandLineArgument(IncludePakPath, ArgumentType.OptionalPositional) + "Loads files, tries to deconvert them and pack into a destination .PAK file"; + + public Argument[] Arguments => [_inputDirectory, _destinationPakFile]; + + public Option[] Options => [_includePakFile]; + + private readonly Argument _inputDirectory = new("input-directory") + { + Description = "Path of the input directory with files" + }; + + private readonly Argument _destinationPakFile = new("destination-pak-file") + { + Description = "Path of the destination directory (creates one if doesn't exist)" + }; + + private readonly Option _includePakFile = new("include-pak-file") + { + Description = "If it's provided, it'll add its content into the new .PAK package" }; private class TemporaryPak : IDisposable { - private readonly string tempPath; - private readonly string resultPath; + private readonly string _tempPath; + + private readonly string _resultPath; - public PakWriter Writer { get; private set; } + public readonly PakWriter Writer; - public TemporaryPak(string finalPath) + public TemporaryPak(FileInfo file) { - tempPath = GetNewTempPackagePath(); - resultPath = finalPath; + _tempPath = GetNewTempPackagePath(); + _resultPath = file.FullName; - var tempPakStream = File.Open(tempPath, FileMode.Create); + var tempPakStream = File.Open(_tempPath, FileMode.Create); Writer = new PakWriter(tempPakStream); } private static string GetNewTempPackagePath() { - return Path.GetTempPath() + "repacker_pak_" + Guid.NewGuid().ToString() + ".pak"; + return Path.GetTempPath() + "repacker_pak_" + Guid.NewGuid() + ".pak"; } + public void Dispose() { Writer.Dispose(); - File.Move(tempPath, resultPath, overwrite: true); + File.Move(_tempPath, _resultPath, overwrite: true); } } - private void IncludePackageIntoWriter(string includePackagePath, PakWriter writer) + private static void IncludePackageIntoWriter(FileInfo includePackage, PakWriter writer) { try { - using var includePackage = PakReader.FromFile(includePackagePath); - foreach (var file in includePackage.ReadFiles()) + using var includePackageReader = PakReader.FromFile(includePackage.FullName); + foreach (var file in includePackageReader.ReadFiles()) { using var fileStream = file.Open(); bool written = writer.WriteFile(file.Path, fileStream, filterExtension: file.FindExtension()); @@ -65,11 +79,11 @@ private void IncludePackageIntoWriter(string includePackagePath, PakWriter write } catch (Exception e) { - Console.Error.WriteLine("Could not fully load included package: $e", e.Message); + Console.Error.WriteLine($"Could not fully load included package: {e.Message}"); } } - private void SortBundlesToPreventInvalidOrdering(ref List fileBundles) + private static void SortBundlesToPreventInvalidOrdering(ref List fileBundles) { // Occasionally, on the process of repacking, we'll need to store multiple files with the same name. // This happens in the base game for effect files stored in Updates.pak. However, the game doesn't @@ -84,19 +98,19 @@ private void SortBundlesToPreventInvalidOrdering(ref List fileBundle if (converterA != null && converterB == null) return 1; if (converterA == null && converterB != null) return -1; - return a.BundlePath.CompareTo(b.BundlePath); + return String.Compare(a.BundlePath, b.BundlePath, StringComparison.InvariantCultureIgnoreCase); }); } - public void Execute(Dictionary args) + public void Execute(ParseResult result) { - var inputPath = args[InputDirectoryPath]; - var outputPackagePath = args[DestinationPakPath]; + var inputDirectory = result.GetRequiredValue(_inputDirectory); + var destinationPakFile = result.GetRequiredValue(_destinationPakFile); - var fileBundlesToAdd = FileBundle.BundleFilesAtPath(inputPath); + var fileBundlesToAdd = FileBundle.BundleFilesAtPath(inputDirectory.FullName); SortBundlesToPreventInvalidOrdering(ref fileBundlesToAdd); - using var tempPak = new TemporaryPak(outputPackagePath); + using var tempPak = new TemporaryPak(destinationPakFile); ConvertToXnbAction.PerformBatchConversion(fileBundlesToAdd, (path, extension, stream, converted) => { @@ -107,13 +121,13 @@ public void Execute(Dictionary args) tempPak.Writer.WriteFile(path, stream, extension); }); - - if (args.TryGetValue(IncludePakPath, out var includePackagePath)) + var includePakFile = result.GetValue(_includePakFile); + if (includePakFile != null) { - IncludePackageIntoWriter(includePackagePath, tempPak.Writer); + IncludePackageIntoWriter(includePakFile, tempPak.Writer); } - Console.WriteLine($"Packed {tempPak.Writer.FileCount} assets into {outputPackagePath}..."); + Console.WriteLine($"Packed {tempPak.Writer.FileCount} assets into {destinationPakFile}..."); } } } \ No newline at end of file diff --git a/Interface/Actions/UnpackAction.cs b/Interface/Actions/UnpackAction.cs index 6ff4751..e37256e 100644 --- a/Interface/Actions/UnpackAction.cs +++ b/Interface/Actions/UnpackAction.cs @@ -1,119 +1,164 @@ -using System; +using System.CommandLine; using FEZRepacker.Core.Conversion; using FEZRepacker.Core.FileSystem; using FEZRepacker.Core.XNB; +using static FEZRepacker.Interface.CommandLineOptions; +using static FEZRepacker.Interface.CommandLineUtils; namespace FEZRepacker.Interface.Actions { - internal abstract class UnpackAction : CommandLineAction + internal class UnpackAction : ICommandLineAction { - private const string PakPath = "pak-path"; - private const string DestinationFolder = "destination-folder"; - private const string UseLegacyAo = "use-legacy-ao"; - private const string UseLegacyTs = "use-legacy-ts"; - public enum UnpackingMode { - Raw, - DecompressedXNB, - Converted + [Aliases("c")] Converted, // Default value + [Aliases("d")] DecompressedXnb, + [Aliases("r")] Raw } - protected abstract UnpackingMode Mode { get; } - public abstract string Name { get; } - public abstract string Description { get; } - public abstract string[] Aliases { get; } - - public CommandLineArgument[] Arguments => new[] { - new CommandLineArgument(PakPath), - new CommandLineArgument(DestinationFolder), - new CommandLineArgument(UseLegacyAo, ArgumentType.Flag), - new CommandLineArgument(UseLegacyTs, ArgumentType.Flag) + + public string Name => "--unpack"; + + public string[] Aliases => ["-u", UnpackDecompressedAlias, UnpackRawAlias]; + + public string Description => + "Unpacks entire .PAK package into specified directory and attempts to process XNB assets.\n\n" + + $"The '{UnpackDecompressedAlias}' and '{UnpackRawAlias}' aliases are provided for backwards compatibility\n" + + $"and they reproduce the command behaviour with the '--mode' flags " + + $"'{ArgumentsOf(UnpackingMode.DecompressedXnb)}' " + + $"and '{ArgumentsOf(UnpackingMode.Raw)}' respectively."; + + public Argument[] Arguments => [_pakFile, _destinationDirectory]; + + public Option[] Options => [_unpackingMode, UseArtObjectLegacyBundles, UseTrileSetLegacyBundles]; + + private readonly Argument _pakFile = new("pak-file") + { + Description = "Source path of the .PAK package" + }; + + private readonly Argument _destinationDirectory = new("destination-directory") + { + Description = "Target path of the destination directory (creates one if doesn't exist)" }; - public void Execute(Dictionary args) + private readonly Option _unpackingMode = new("--mode", "-m") + { + Description = "Unpacking mode\n" + + $" <{ArgumentsOf(UnpackingMode.Converted)}> (default): Convert XNB assets into their corresponding format\n" + + $" <{ArgumentsOf(UnpackingMode.DecompressedXnb)}>': Decompress XNB assets, but do not convert them\n" + + $" <{ArgumentsOf(UnpackingMode.Raw)}>': Leave XNB assets in their original form\n", + CustomParser = CustomAliasedEnumParser + }; + + private const string UnpackDecompressedAlias = "--unpack-decompressed"; + + private const string UnpackRawAlias = "--unpack-raw"; + + public void Execute(ParseResult result) { - var pakPath = args[PakPath]; - var outputDir = args[DestinationFolder]; + var pakFile = result.GetRequiredValue(_pakFile); + var destinationDirectory = result.GetRequiredValue(_destinationDirectory); + + UnpackingMode unpackingMode; + switch (result.CommandResult.IdentifierToken.Value) + { + // For backward compatibility, the value of the "--mode" option + // will be overwritten when using these older commands. + case UnpackDecompressedAlias: + Console.WriteLine($"Warning! The '{_unpackingMode.Name}' option will be ignored."); + unpackingMode = UnpackingMode.DecompressedXnb; + break; + case UnpackRawAlias: + Console.WriteLine($"Warning! The '{_unpackingMode.Name}' option will be ignored."); + unpackingMode = UnpackingMode.Raw; + break; + default: + unpackingMode = result.GetValue(_unpackingMode); + break; + } + var settings = new FormatConverterSettings { - UseLegacyArtObjectBundle = args.ContainsKey(UseLegacyAo), - UseLegacyTrileSetBundle = args.ContainsKey(UseLegacyTs) + UseLegacyArtObjectBundle = result.GetValue(UseArtObjectLegacyBundles), + UseLegacyTrileSetBundle = result.GetValue(UseTrileSetLegacyBundles) }; - - UnpackPackage(pakPath, outputDir, Mode, settings); + UnpackPackage(pakFile, destinationDirectory, unpackingMode, settings); } public static FileBundle UnpackFile(string extension, Stream data, UnpackingMode mode, FormatConverterSettings settings) { - if (extension != ".xnb" || mode == UnpackingMode.Raw) + if (extension != ".xnb") { return FileBundle.Single(data, extension); } - else if (mode == UnpackingMode.DecompressedXNB) - { - return FileBundle.Single(XnbCompressor.Decompress(data), ".xnb"); - } - else if (mode == UnpackingMode.Converted) + + switch (mode) { - var initialStreamPosition = data.Position; - FileBundle outputBundle; - try - { - var outputData = XnbSerializer.Deserialize(data)!; - outputBundle = FormatConversion.Convert(outputData, settings); - Console.WriteLine($" {outputData.GetType().Name} converted into {outputBundle.MainExtension} format."); - } - catch (Exception ex) - { - Console.WriteLine($" Cannot deserialize XNB file: {ex.Message}. Saving raw file instead."); - data.Seek(initialStreamPosition, SeekOrigin.Begin); + case UnpackingMode.Raw: return FileBundle.Single(data, extension); - } - return outputBundle; - } - else - { - return new FileBundle(); + case UnpackingMode.DecompressedXnb: + return FileBundle.Single(XnbCompressor.Decompress(data), ".xnb"); + + case UnpackingMode.Converted: + var initialStreamPosition = data.Position; + FileBundle outputBundle; + try + { + var outputData = XnbSerializer.Deserialize(data)!; + outputBundle = FormatConversion.Convert(outputData, settings); + Console.WriteLine( + $" {outputData.GetType().Name} converted into {outputBundle.MainExtension} format."); + } + catch (Exception ex) + { + Console.WriteLine($" Cannot deserialize XNB file: {ex.Message}. Saving raw file instead."); + data.Seek(initialStreamPosition, SeekOrigin.Begin); + return FileBundle.Single(data, extension); + } + return outputBundle; + + default: + return new FileBundle(); } } - public static void UnpackPackage(string pakPath, string outputDir, UnpackingMode mode, FormatConverterSettings settings) + public static void UnpackPackage(FileInfo pakFile, DirectoryInfo outputDir, UnpackingMode mode, FormatConverterSettings settings) { - if (Path.GetExtension(pakPath) != ".pak") + if (pakFile.Extension != ".pak") { throw new Exception("A path must lead to a .PAK file."); } - if (!Directory.Exists(outputDir)) + if (!outputDir.Exists) { - Directory.CreateDirectory(outputDir); + outputDir.Create(); } - using var pakStream = File.OpenRead(pakPath); + using var pakStream = File.OpenRead(pakFile.FullName); using var pakReader = new PakReader(pakStream); - Console.WriteLine($"Unpacking archive {pakPath} containing {pakReader.FileCount} files..."); + Console.WriteLine($"Unpacking archive {pakFile} containing {pakReader.FileCount} files..."); int filesDone = 0; - foreach (var pakFile in pakReader.ReadFiles()) + foreach (var fileRecord in pakReader.ReadFiles()) { - var extension = pakFile.FindExtension(); + var extension = fileRecord.FindExtension(); Console.WriteLine( $"({filesDone + 1}/{pakReader.FileCount})" + - $"{pakFile.Path} ({(extension.Length == 0 ? "unknown" : extension)} file," + - $"size: {pakFile.Length} bytes)" + $"{fileRecord.Path} ({(extension.Length == 0 ? "unknown" : extension)} file," + + $"size: {fileRecord.Length} bytes)" ); try { - using var fileStream = pakFile.Open(); + using var fileStream = fileRecord.Open(); using var outputBundle = UnpackFile(extension, fileStream, mode, settings); - outputBundle.BundlePath = Path.Combine(outputDir, pakFile.Path); + outputBundle.BundlePath = Path.Combine(outputDir.FullName, fileRecord.Path); Directory.CreateDirectory(Path.GetDirectoryName(outputBundle.BundlePath) ?? ""); foreach (var outputFile in outputBundle.Files) @@ -125,8 +170,9 @@ public static void UnpackPackage(string pakPath, string outputDir, UnpackingMode } catch (Exception ex) { - Console.Error.WriteLine($"Unable to unpack {pakFile.Path} - {ex.Message}"); + Console.Error.WriteLine($"Unable to unpack {fileRecord.Path} - {ex.Message}"); } + filesDone++; } } diff --git a/Interface/Actions/UnpackConvertAction.cs b/Interface/Actions/UnpackConvertAction.cs deleted file mode 100644 index 72da328..0000000 --- a/Interface/Actions/UnpackConvertAction.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace FEZRepacker.Interface.Actions -{ - internal class UnpackConvertAction : UnpackAction - { - protected override UnpackingMode Mode => UnpackingMode.Converted; - public override string Name => "--unpack"; - public override string[] Aliases => new[] { "-u" }; - public override string Description => - "Unpacks entire .PAK package into specified directory (creates one if doesn't exist) " + - "and attempts to convert XNB assets into their corresponding format in the process."; - } -} diff --git a/Interface/Actions/UnpackDecompressedAction.cs b/Interface/Actions/UnpackDecompressedAction.cs deleted file mode 100644 index b0bf09d..0000000 --- a/Interface/Actions/UnpackDecompressedAction.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace FEZRepacker.Interface.Actions -{ - internal class UnpackDecompressedAction : UnpackAction - { - protected override UnpackingMode Mode => UnpackingMode.DecompressedXNB; - public override string Name => "--unpack-decompressed"; - public override string[] Aliases => new string[] {}; - public override string Description => - "Unpacks entire .PAK package into specified directory (creates one if doesn't exist)." + - "and attempts to decompress all XNB assets, but does not convert them."; - } -} diff --git a/Interface/Actions/UnpackGameAction.cs b/Interface/Actions/UnpackGameAction.cs index e3e94a1..6ecc51a 100644 --- a/Interface/Actions/UnpackGameAction.cs +++ b/Interface/Actions/UnpackGameAction.cs @@ -1,60 +1,76 @@ -using FEZRepacker.Core.Conversion; +using System.CommandLine; + +using FEZRepacker.Core.Conversion; using static FEZRepacker.Interface.Actions.UnpackAction; +using static FEZRepacker.Interface.CommandLineOptions; namespace FEZRepacker.Interface.Actions { - internal class UnpackGameAction : CommandLineAction + internal class UnpackGameAction : ICommandLineAction { - private const string FezContentDirectory = "fez-content-directory"; - private const string DestinationFolder = "destination-folder"; - private const string UseLegacyAo = "use-legacy-ao"; - private const string UseLegacyTs = "use-legacy-ts"; - public string Name => "--unpack-fez-content"; - public string[] Aliases => new[] { "-g" }; + public string[] Aliases => ["-g"]; public string Description => - "Unpacks and converts all game assets into specified directory (creates one if doesn't exist)."; + "Unpacks and converts all game assets into specified directory"; + + public Argument[] Arguments => [_fezContentDirectory, _destinationDirectory]; - public CommandLineArgument[] Arguments => new[] { - new CommandLineArgument(FezContentDirectory), - new CommandLineArgument(DestinationFolder), - new CommandLineArgument(UseLegacyAo, ArgumentType.Flag), - new CommandLineArgument(UseLegacyTs, ArgumentType.Flag) + public Option[] Options => [UseArtObjectLegacyBundles, UseTrileSetLegacyBundles]; + + private readonly Argument _fezContentDirectory = new("fez-content-directory") + { + Description = "Source path of the content directory (usually, it's 'Content' in the game's directory)" }; - public void Execute(Dictionary args) + private readonly Argument _destinationDirectory = new("destination-directory") { - var contentPath = args[FezContentDirectory]; - var outputDir = args[DestinationFolder]; + Description = "Target path of the destination directory (creates one if doesn't exist)" + }; - var packagePaths = new string[] { "Essentials.pak", "Music.pak", "Other.pak", "Updates.pak" } - .Select(path => Path.Combine(contentPath, path)).ToArray(); + private static readonly string[] KnownPaks = + [ + "Essentials.pak", + "Music.pak", + "Other.pak", + "Updates.pak" + ]; + + private const string MusicPak = "Music.pak"; + + public void Execute(ParseResult result) + { + var fezContentDirectory = result.GetRequiredValue(_fezContentDirectory); + var destinationDirectory = result.GetRequiredValue(_destinationDirectory); + + var packagePaths = KnownPaks + .Select(pak => new FileInfo(Path.Combine(fezContentDirectory.FullName, pak))) + .ToArray(); foreach (var packagePath in packagePaths) { - if (!File.Exists(packagePath)) + if (!packagePath.Exists) { - throw new Exception($"Given directory is not FEZ's Content directory (missing {Path.GetFileName(packagePath)})."); + throw new Exception($"Given directory is not FEZ's Content directory (missing {packagePath})."); } } - + var settings = new FormatConverterSettings { - UseLegacyArtObjectBundle = args.ContainsKey(UseLegacyAo), - UseLegacyTrileSetBundle = args.ContainsKey(UseLegacyTs) + UseLegacyArtObjectBundle = result.GetValue(UseArtObjectLegacyBundles), + UseLegacyTrileSetBundle = result.GetValue(UseTrileSetLegacyBundles) }; foreach (var packagePath in packagePaths) { - var actualOutputDir = outputDir; - if (packagePath.EndsWith("Music.pak")) + var actualOutputDir = destinationDirectory; + if (packagePath.FullName.EndsWith(MusicPak)) { // Special Repacker behaviour - instead of dumping music tracks in // the same folder as other assets, put them in separate music folder. - actualOutputDir = Path.Combine(outputDir, "music"); + actualOutputDir = new DirectoryInfo(Path.Combine(destinationDirectory.FullName, "music")); } UnpackPackage(packagePath, actualOutputDir, UnpackingMode.Converted, settings); } diff --git a/Interface/Actions/UnpackRawAction.cs b/Interface/Actions/UnpackRawAction.cs deleted file mode 100644 index 07deacf..0000000 --- a/Interface/Actions/UnpackRawAction.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace FEZRepacker.Interface.Actions -{ - internal class UnpackRawAction : UnpackAction - { - protected override UnpackingMode Mode => UnpackingMode.Raw; - public override string Name => "--unpack-raw"; - public override string[] Aliases => new string[] {}; - public override string Description => - "Unpacks entire .PAK package into specified directory (creates one " + - "if doesn't exist) leaving XNB assets in their original form."; - } -} diff --git a/Interface/AliasesAttribute.cs b/Interface/AliasesAttribute.cs new file mode 100644 index 0000000..04fdd55 --- /dev/null +++ b/Interface/AliasesAttribute.cs @@ -0,0 +1,11 @@ +namespace FEZRepacker.Interface +{ + /// + /// Used to store aliases. + /// + [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] + internal class AliasesAttribute(params string[] aliases) : Attribute + { + public string[] Aliases { get; } = aliases; + } +} \ No newline at end of file diff --git a/Interface/CommandLineAction.cs b/Interface/CommandLineAction.cs deleted file mode 100644 index b3c2af1..0000000 --- a/Interface/CommandLineAction.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace FEZRepacker.Interface -{ - internal interface CommandLineAction - { - public string Name { get; } - public string[] Aliases { get; } - public string Description { get; } - public CommandLineArgument[] Arguments { get; } - public void Execute(Dictionary args); - - } -} diff --git a/Interface/CommandLineArgument.cs b/Interface/CommandLineArgument.cs deleted file mode 100644 index 7c63ea2..0000000 --- a/Interface/CommandLineArgument.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace FEZRepacker.Interface -{ - public enum ArgumentType - { - RequiredPositional, - OptionalPositional, - Flag, - } - - public struct CommandLineArgument - { - public readonly string Name; - - public readonly ArgumentType Type; - - public readonly string[] Aliases; - - public readonly string Description; - - public CommandLineArgument( - string name, - ArgumentType type = ArgumentType.RequiredPositional, - string description = "", - string[]? aliases = null) - { - Name = name; - Type = type; - Aliases = aliases ?? []; - Description = description; - } - } -} \ No newline at end of file diff --git a/Interface/CommandLineInterface.cs b/Interface/CommandLineInterface.cs index 3202cfc..6fc23d8 100644 --- a/Interface/CommandLineInterface.cs +++ b/Interface/CommandLineInterface.cs @@ -1,199 +1,79 @@ -using FEZRepacker.Interface.Actions; +using System.CommandLine; + +using FEZRepacker.Core; +using FEZRepacker.Interface.Actions; namespace FEZRepacker.Interface { internal static class CommandLineInterface { - public static readonly CommandLineAction[] Commands = + private static readonly ICommandLineAction[] Actions = [ - new HelpAction(), new ListPackageContentAction(), - new UnpackConvertAction(), - new UnpackRawAction(), - new UnpackDecompressedAction(), new PackAction(), + new UnpackAction(), new UnpackGameAction(), new ConvertFromXnbAction(), new ConvertToXnbAction() ]; - public static CommandLineAction? FindCommand(string name) - { - return Commands.FirstOrDefault(command => command.Name == name || command.Aliases.Contains(name)); - } - /// The command to execute /// True if a command was executed, false otherwise. - public static bool ParseCommandLine(string[] args) + public static int ParseCommandLine(string[] args) { - if (args.Length == 0) return false; - - var command = FindCommand(args[0]); - if (command == null) - { - Console.WriteLine($"Unknown command \"{args[0]}\"."); - Console.WriteLine($"Type \"FEZRepacker.exe --help\" for a list of commands."); - return false; - } - - var parsedArgs = ParseArguments(command, args.Skip(1).ToArray()); - if (parsedArgs == null) - { - Console.WriteLine($"Invalid usage for command \"{args[0]}\"."); - Console.WriteLine( - $"Use \"FEZRepacker.exe --help {args[0]}\" for a usage instruction for that command."); - return false; - } - - try - { - command.Execute(parsedArgs); - } - catch (Exception ex) - { - #if DEBUG - throw; - #else - Console.Error.WriteLine($"Error while executing command: {ex.Message}"); - #endif - } - - return true; + return InitializeInterface(Metadata.Version).Parse(args).Invoke(); } /// /// Runs interactive mode which repeatedly requests user input and parses it as commands. /// - public static void RunInteractiveMode() + public static int RunInteractiveMode() { + var command = InitializeInterface(); + Console.WriteLine($"=== {Metadata.Version} ==="); Console.Write('\a'); //alert user - - ShowInteractiveModeHelp(); - while (true) { Console.WriteLine(); Console.Write("> FEZRepacker.exe "); string? line = Console.ReadLine(); - if (line == null) return; // No lines remain to read. Exit the program. + if (line == null) break; // No lines remain to read. Exit the program. var args = ParseArguments(line); - if (ParseInteractiveModeCommands(args, out var shouldTerminate)) { if (shouldTerminate) break; continue; } - - if (args.Length > 0) - { - // Handle help command specifically - if (args[0].ToLower() == "help" || args[0] == "--help" || args[0] == "-h") - { - if (args.Length > 1) - { - // Show help for a specific command - var command = FindCommand(args[1]); - if (command != null) - { - ShowCommandHelp(command); - } - else - { - Console.WriteLine($"Unknown command \"{args[1]}\"."); - } - } - else - { - ShowGeneralHelp(); - } - - continue; - } - - if (ParseCommandLine(args)) - { - continue; - } - } - - ShowInteractiveModeHelp(); + command.Parse(args).Invoke(); } - } - - private static void ShowInteractiveModeHelp() - { - Console.WriteLine("To get usage help, use 'help' or '--help'"); - Console.WriteLine("For help with a specific command, use 'help '"); - Console.WriteLine("To exit, use 'exit'"); - } - private static void ShowGeneralHelp() - { - Console.WriteLine("Available commands:"); - Console.WriteLine(); - foreach (var command in Commands) - { - Console.WriteLine($" {command.Name} - {command.Description}"); - if (command.Aliases.Length < 1) continue; - Console.WriteLine($" Aliases: {string.Join(", ", command.Aliases)}"); - Console.WriteLine(); - } + return 0; } - private static void ShowCommandHelp(CommandLineAction command) + private static RootCommand InitializeInterface(string description = "") { - Console.WriteLine($"Command: {command.Name}"); - if (command.Aliases.Length > 0) + var rootCommand = new RootCommand(description); + foreach (var action in Actions) { - Console.WriteLine($"Aliases: {string.Join(", ", command.Aliases)}"); - } - - Console.WriteLine($"Description: {command.Description}"); - Console.WriteLine("Usage:"); - - var usage = $"FEZRepacker.exe {command.Name}"; - foreach (var arg in command.Arguments) - { - if (arg.Type == ArgumentType.Flag) + var command = new Command(action.Name, action.Description); + foreach (var alias in action.Aliases) { - usage += arg.Type == ArgumentType.RequiredPositional ? " --" : " [--"; - usage += arg.Name; - if (arg.Aliases.Length > 0) - { - usage += $" (-{string.Join(" -", arg.Aliases)})"; - } - - usage += arg.Type == ArgumentType.RequiredPositional ? "" : "]"; + command.Aliases.Add(alias); } - else + foreach (var argument in action.Arguments) { - usage += arg.Type == ArgumentType.RequiredPositional ? $" {arg.Name}" : $" [{arg.Name}]"; + command.Arguments.Add(argument); } - } - - Console.WriteLine(usage); - - if (command.Arguments.Length <= 0) return; - - Console.WriteLine("Arguments:"); - foreach (var arg in command.Arguments) - { - var argInfo = $" {arg.Name}"; - if (arg is { Type: ArgumentType.Flag, Aliases.Length: > 0 }) - { - argInfo += $" (Aliases: {string.Join(", ", arg.Aliases.Select(a => $"-{a}"))})"; - } - - argInfo += arg.Type == ArgumentType.RequiredPositional ? " (Required)" : " (Optional)"; - if (!string.IsNullOrEmpty(arg.Description)) + foreach (var option in action.Options) { - argInfo += $": {arg.Description}"; + command.Options.Add(option); } - - Console.WriteLine(argInfo); + command.SetAction(action.Execute); + rootCommand.Add(command); } + return rootCommand; } private static bool ParseInteractiveModeCommands(string[] args, out bool terminationRequested) @@ -215,118 +95,17 @@ private static bool ParseInteractiveModeCommands(string[] args, out bool termina Console.WriteLine("Exiting..."); return true; default: + Console.WriteLine(); return false; } } - private static Dictionary? ParseArguments(CommandLineAction command, string[] args) - { - var result = new Dictionary(); - var positionalArgs = new List(); - var flags = new HashSet(); - - // Get all argument definitions for this command - var requiredPositional = command.Arguments - .Where(a => a.Type == ArgumentType.RequiredPositional) - .ToArray(); - - var optionalPositional = command.Arguments - .Where(a => a.Type == ArgumentType.OptionalPositional) - .ToArray(); - - var flagArguments = command.Arguments - .Where(a => a.Type == ArgumentType.Flag) - .ToArray(); - - // Create a set of all valid flag names and aliases - var allFlagNames = flagArguments - .SelectMany(f => new[] { f.Name }.Concat(f.Aliases)) - .ToHashSet(); - - // Parse arguments in order, respecting the command's argument definitions - foreach (var arg in args) - { - if (!(arg.StartsWith("--") || arg.StartsWith("-"))) - { - positionalArgs.Add(arg); - continue; - } - - string flagContent; - if (arg.StartsWith("--")) // Long flag - { - flagContent = arg[2..]; - } - else if (arg.StartsWith("-")) // Short flag - { - flagContent = arg[1..]; - } - else - { - positionalArgs.Add(arg); - continue; - } - - if (flagContent.Contains('=')) - { - var parts = flagContent.Split('=', 2); - if (allFlagNames.Contains(parts[0])) - { - result[parts[0]] = parts[1]; - } - else - { - positionalArgs.Add(arg); - } - } - else if (allFlagNames.Contains(flagContent)) - { - flags.Add(flagContent); - } - else - { - positionalArgs.Add(arg); - } - } - - // Not enough required positional arguments - if (positionalArgs.Count < requiredPositional.Length) - return null; - - // Too many positional arguments - if (positionalArgs.Count > requiredPositional.Length + optionalPositional.Length) - return null; - - for (int i = 0; i < requiredPositional.Length; i++) - { - result[requiredPositional[i].Name] = positionalArgs[i]; - } - - for (int i = 0; i < optionalPositional.Length; i++) - { - if (i + requiredPositional.Length < positionalArgs.Count) - { - result[optionalPositional[i].Name] = positionalArgs[i + requiredPositional.Length]; - } - } - - foreach (var flagArg in command.Arguments.Where(a => a.Type == ArgumentType.Flag)) - { - if (flags.Contains(flagArg.Name) || flagArg.Aliases.Any(flags.Contains)) - { - result[flagArg.Name] = "true"; - } - } - - return result; - } - private static string[] ParseArguments(string argumentsString) { return argumentsString.Split('"') - .SelectMany((element, index) => (index % 2 == 0) ? element.Split(' ') : new[] { element }) + .SelectMany((element, index) => (index % 2 == 0) ? element.Split(' ') : [element]) .Where(s => !string.IsNullOrEmpty(s)) .ToArray(); } } -} \ No newline at end of file +} diff --git a/Interface/CommandLineOptions.cs b/Interface/CommandLineOptions.cs new file mode 100644 index 0000000..0cde04a --- /dev/null +++ b/Interface/CommandLineOptions.cs @@ -0,0 +1,17 @@ +using System.CommandLine; + +namespace FEZRepacker.Interface +{ + internal static class CommandLineOptions + { + public static readonly Option UseArtObjectLegacyBundles = new("--use-legacy-ao", "-lao") + { + Description = "Use a legacy bundle format with separate files instead of all-in-one glTF bundle for Art Objects." + }; + + public static readonly Option UseTrileSetLegacyBundles = new("--use-legacy-ts", "-lts") + { + Description = "Use a legacy bundle format with separate files instead of all-in-one glTF bundle for Trile Sets." + }; + } +} \ No newline at end of file diff --git a/Interface/CommandLineUtils.cs b/Interface/CommandLineUtils.cs new file mode 100644 index 0000000..29b4885 --- /dev/null +++ b/Interface/CommandLineUtils.cs @@ -0,0 +1,67 @@ +using System.CommandLine.Parsing; + +namespace FEZRepacker.Interface +{ + public static class CommandLineUtils + { + /// + /// Parses the enum argument result into enum value using the attribute. + /// If the attribute was not specified, the parser uses default behaviour of the parser. + /// + /// CLI Argument parsing result + /// Enum type template + /// An enum value + /// Throws if the template type is not an enum. + public static T CustomAliasedEnumParser(ArgumentResult result) where T : struct, IConvertible + { + var type = typeof(T); + if (!type.IsEnum) + { + throw new ArgumentException("T must be an enumerated type"); + } + + foreach (var enumValue in Enum.GetValues(type)) + { + var enumName = Enum.GetName(type, enumValue)!; + var memberInfo = type.GetMember(enumValue.ToString()!); + var attributes = memberInfo[0].GetCustomAttributes(typeof(AliasesAttribute), false); + + if (attributes.Length > 0) + { + var aliases = (attributes.First() as AliasesAttribute)!.Aliases; + var aliasFound = aliases.Any(a => result.Tokens + .Any(t => t.Value.ToLower().Equals(a.ToLower()))); + if (aliasFound) return (T)enumValue; + } + + var nameFound = result.Tokens.Any(t => t.Value.ToLower().Equals(enumName.ToLower())); + if (nameFound) return (T)enumValue; + } + + return default; + } + + /// + /// Returns formatted argument and aliases list of provided enum value. + /// + /// An enum value + /// Formatted list of available arguments + public static string ArgumentsOf(Enum enumValue) + { + var type = enumValue.GetType(); + var name = Enum.GetName(type, enumValue)!; + + var memberInfo = type.GetMember(enumValue.ToString()!); + var attributes = memberInfo[0].GetCustomAttributes(typeof(AliasesAttribute), false); + + var arguments = new List { name }; + if (attributes.Length > 0) + { + var aliases = (attributes.First() as AliasesAttribute)!.Aliases; + arguments.AddRange(aliases); + } + + return String.Join("|", arguments); + } + } +} \ No newline at end of file diff --git a/Interface/FEZRepacker.Interface.csproj b/Interface/FEZRepacker.Interface.csproj index c92c9ed..76f9de8 100644 --- a/Interface/FEZRepacker.Interface.csproj +++ b/Interface/FEZRepacker.Interface.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net9.0 $(SolutionName) latest @@ -14,10 +14,15 @@ false true true + true + + + + diff --git a/Interface/ICommandLineAction.cs b/Interface/ICommandLineAction.cs new file mode 100644 index 0000000..2a5c499 --- /dev/null +++ b/Interface/ICommandLineAction.cs @@ -0,0 +1,19 @@ +using System.CommandLine; + +namespace FEZRepacker.Interface +{ + internal interface ICommandLineAction + { + string Name { get; } + + string[] Aliases { get; } + + string Description { get; } + + Argument[] Arguments { get; } + + Option[] Options { get; } + + void Execute(ParseResult result); + } +} diff --git a/Interface/Program.cs b/Interface/Program.cs index 8c1a673..9ad752e 100644 --- a/Interface/Program.cs +++ b/Interface/Program.cs @@ -1,21 +1,12 @@ namespace FEZRepacker.Interface { - internal class Program + static class Program { - static void Main(string[] args) + static int Main(string[] args) { - var version = Core.Metadata.Version; - // showoff - Console.WriteLine($"=== {version} ===\n"); - - if (args.Length > 0) - { - CommandLineInterface.ParseCommandLine(args); - } - else - { - CommandLineInterface.RunInteractiveMode(); - } + return args.Length > 0 + ? CommandLineInterface.ParseCommandLine(args) + : CommandLineInterface.RunInteractiveMode(); } } diff --git a/Tests/FEZRepacker.Tests.csproj b/Tests/FEZRepacker.Tests.csproj index f47bf3e..1e26d83 100644 --- a/Tests/FEZRepacker.Tests.csproj +++ b/Tests/FEZRepacker.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net9.0 enable enable latest