From 7ec34a9c14b61095ee717d05e8cf00fb63fd82ab Mon Sep 17 00:00:00 2001 From: "Nathan V." Date: Fri, 10 Oct 2025 13:31:39 -0400 Subject: [PATCH] Refactor CLI commands to delegate to reusable operations --- .../CLI/Commands/ExportResourceCommand.cs | 199 ++++--- .../CLI/Commands/ImportResourceCommand.cs | 321 ++++-------- .../CLI/Commands/ImportStringTableCommand.cs | 107 +--- Volatility/CLI/Commands/PortTextureCommand.cs | 490 +----------------- .../Resources/ExportResourceOperation.cs | 37 ++ .../Resources/ImportResourceOperation.cs | 166 ++++++ .../Resources/LoadResourceOperation.cs | 25 + .../Resources/PortTextureOperation.cs | 438 ++++++++++++++++ .../Resources/SaveResourceOperation.cs | 35 ++ .../ImportStringTableOperation.cs | 86 +++ .../LoadResourceDictionaryOperation.cs | 30 ++ .../MergeStringTableEntriesOperation.cs | 38 ++ .../StringTables/StringTableResourceEntry.cs | 7 + 13 files changed, 1048 insertions(+), 931 deletions(-) create mode 100644 Volatility/Operations/Resources/ExportResourceOperation.cs create mode 100644 Volatility/Operations/Resources/ImportResourceOperation.cs create mode 100644 Volatility/Operations/Resources/LoadResourceOperation.cs create mode 100644 Volatility/Operations/Resources/PortTextureOperation.cs create mode 100644 Volatility/Operations/Resources/SaveResourceOperation.cs create mode 100644 Volatility/Operations/StringTables/ImportStringTableOperation.cs create mode 100644 Volatility/Operations/StringTables/LoadResourceDictionaryOperation.cs create mode 100644 Volatility/Operations/StringTables/MergeStringTableEntriesOperation.cs create mode 100644 Volatility/Operations/StringTables/StringTableResourceEntry.cs diff --git a/Volatility/CLI/Commands/ExportResourceCommand.cs b/Volatility/CLI/Commands/ExportResourceCommand.cs index 84caaf1..8d3547e 100644 --- a/Volatility/CLI/Commands/ExportResourceCommand.cs +++ b/Volatility/CLI/Commands/ExportResourceCommand.cs @@ -1,5 +1,4 @@ -using System.Runtime.Serialization; - +using Volatility.Operations.Resources; using Volatility.Resources; using Volatility.Utilities; @@ -7,144 +6,120 @@ namespace Volatility.CLI.Commands; -internal partial class ExportResourceCommand : ICommand +internal class ExportResourceCommand : ICommand { - public static string CommandToken => "ExportResource"; - public static string CommandDescription => "Exports information and relevant data from an imported/created resource into a platform's format."; - public static string CommandParameters => "--recurse --overwrite --type= --format= --respath= --outpath="; - - public string? Format { get; set; } - public string? ResourcePath { get; set; } - public string? OutputPath { get; set; } - public bool Overwrite { get; set; } - public bool Recursive { get; set; } - - public async Task Execute() - { + public static string CommandToken => "ExportResource"; + public static string CommandDescription => "Exports information and relevant data from an imported/created resource into a platform's format."; + public static string CommandParameters => "--recurse --overwrite --type= --format= --respath= --outpath="; + + public string? Format { get; set; } + public string? ResourcePath { get; set; } + public string? OutputPath { get; set; } + public bool Overwrite { get; set; } + public bool Recursive { get; set; } + + public async Task Execute() + { if (string.IsNullOrEmpty(Format)) { Console.WriteLine("Error: No resource path specified! (--respath)"); return; } if (string.IsNullOrEmpty(ResourcePath)) - { - Console.WriteLine("Error: No resource path specified! (--respath)"); - return; - } - if (string.IsNullOrEmpty(OutputPath)) - { - Console.WriteLine("Error: No output path specified! (--outpath)"); - return; - } + { + Console.WriteLine("Error: No resource path specified! (--respath)"); + return; + } + if (string.IsNullOrEmpty(OutputPath)) + { + Console.WriteLine("Error: No output path specified! (--outpath)"); + return; + } string filePath = $"" + - $"{ Path.Combine - ( - GetEnvironmentDirectory(EnvironmentDirectory.Resources), - ResourcePath - ) - }"; + $"{ Path.Combine + ( + GetEnvironmentDirectory(EnvironmentDirectory.Resources), + ResourcePath + ) + }"; string[] sourceFiles = ICommand.GetFilePathsInDirectory(filePath, ICommand.TargetFileType.Any, Recursive); - if (sourceFiles.Length == 0) - { - Console.WriteLine($"Error: No valid file(s) found at the specified path ({ResourcePath}). Ensure the path exists and spaces are properly enclosed. (--path)"); - return; - } - - List tasks = new List(); - foreach (string sourceFile in sourceFiles) - { - Console.WriteLine(sourceFile); - - tasks.Add(Task.Run(async () => - { - FileAttributes fileAttributes; - try - { - fileAttributes = File.GetAttributes(sourceFile); - } - catch (FileNotFoundException) - { - Console.WriteLine("Error: Invalid file import path specified!"); - return; - } - catch (DirectoryNotFoundException) - { - Console.WriteLine("Error: Can not find directory for specified import path!"); - return; - } - catch (Exception e) - { - Console.WriteLine($"Error: Caught file exception: {e.Message}"); - return; - } + if (sourceFiles.Length == 0) + { + Console.WriteLine($"Error: No valid file(s) found at the specified path ({ResourcePath}). Ensure the path exists and spaces are properly enclosed. (--path)"); + return; + } if (!TypeUtilities.TryParseEnum(Format, out Platform platform)) { throw new InvalidPlatformException("Error: Invalid file format specified!"); } - if (!TypeUtilities.TryParseEnum(Path.GetExtension(filePath).TrimStart('.'), out ResourceType resourceType)) + var loadOperation = new LoadResourceOperation(); + var exportOperation = new ExportResourceOperation(); + + List tasks = new List(); + foreach (string sourceFile in sourceFiles) + { + Console.WriteLine(sourceFile); + + tasks.Add(Task.Run(async () => + { + try + { + File.GetAttributes(sourceFile); + } + catch (FileNotFoundException) + { + Console.WriteLine("Error: Invalid file import path specified!"); + return; + } + catch (DirectoryNotFoundException) + { + Console.WriteLine("Error: Can not find directory for specified import path!"); + return; + } + catch (Exception e) + { + Console.WriteLine($"Error: Caught file exception: {e.Message}"); + return; + } + + if (!TypeUtilities.TryParseEnum(Path.GetExtension(sourceFile).TrimStart('.'), out ResourceType resourceType)) { Console.WriteLine("Error: Resource type is invalid!"); return; } - string yaml = File.ReadAllText(sourceFile); - - Resource resource = ResourceFactory.CreateResource(resourceType, platform, ""); - try - { - resource = (Resource?)ResourceYamlDeserializer.DeserializeResource(resource.GetType(), yaml); - if (resource is not Resource) - { - throw new SerializationException(); - } + Resource resource; + try + { + resource = await loadOperation.ExecuteAsync(sourceFile, resourceType, platform); } - catch (Exception e) - { + catch (Exception e) + { Console.WriteLine($"ERROR: Unable to deserialize {Path.GetFileName(sourceFile)} as {resourceType}!\nMessage from {e.TargetSite}: {e.Message}.\nStack Trace:\n{e.StackTrace}"); + return; } - Directory.CreateDirectory(Path.GetDirectoryName(OutputPath)); - - using (FileStream fs = new(OutputPath, FileMode.Create)) - { - Endian endian = resource.GetResourceEndian() != Endian.Agnostic - ? resource.GetResourceEndian() - : EndianMapping.GetDefaultEndian(platform); - - using (EndianAwareBinaryWriter writer = new(fs, endian)) - { - // The way this is handled is pending a pipeline rewrite - switch (resource) - { - case TextureBase texture: - texture.PushAll(); - goto default; - default: - resource.WriteToStream(writer); - break; - } - } - } + await exportOperation.ExecuteAsync(resource, OutputPath, platform); Console.WriteLine($"Exported {Path.GetFileName(ResourcePath)} as {Path.GetFullPath(OutputPath)}."); - })); - } - await Task.WhenAll(tasks); - } + })); + } + await Task.WhenAll(tasks); + } public void SetArgs(Dictionary args) - { - Format = (args.TryGetValue("format", out object? format) ? format as string : "")?.ToUpper(); - ResourcePath = args.TryGetValue("respath", out object? respath) ? respath as string : ""; - OutputPath = args.TryGetValue("outpath", out object? outpath) ? outpath as string : ""; - Overwrite = args.TryGetValue("overwrite", out var ow) && (bool)ow; - Recursive = args.TryGetValue("recurse", out var re) && (bool)re; - } - - public ExportResourceCommand() { } -} \ No newline at end of file + { + Format = (args.TryGetValue("format", out object? format) ? format as string : "")?.ToUpper(); + ResourcePath = args.TryGetValue("respath", out object? respath) ? respath as string : ""; + OutputPath = args.TryGetValue("outpath", out object? outpath) ? outpath as string : ""; + Overwrite = args.TryGetValue("overwrite", out var ow) && (bool)ow; + Recursive = args.TryGetValue("recurse", out var re) && (bool)re; + } + + public ExportResourceCommand() { } +} diff --git a/Volatility/CLI/Commands/ImportResourceCommand.cs b/Volatility/CLI/Commands/ImportResourceCommand.cs index b4123bd..80bab56 100644 --- a/Volatility/CLI/Commands/ImportResourceCommand.cs +++ b/Volatility/CLI/Commands/ImportResourceCommand.cs @@ -1,8 +1,4 @@ -using System.Diagnostics; -using System.Text.RegularExpressions; - -using YamlDotNet.Serialization; - +using Volatility.Operations.Resources; using Volatility.Resources; using Volatility.Utilities; @@ -10,245 +6,104 @@ namespace Volatility.CLI.Commands; -internal partial class ImportResourceCommand : ICommand +internal class ImportResourceCommand : ICommand { - public static string CommandToken => "ImportResource"; - public static string CommandDescription => "Imports information and relevant data from a specified platform's resource into a standardized format."; - public static string CommandParameters => "--recurse --overwrite --type= --format= --path="; - - public string? ResType { get; set; } - public string? Format { get; set; } - public string? ImportPath { get; set; } - public bool Overwrite { get; set; } - public bool Recursive { get; set; } - - public async Task Execute() - { - if (ResType == "AUTO") - { - Console.WriteLine("Error: Automatic typing is not supported yet! Please specify a type (--type)"); - return; - } - - if (string.IsNullOrEmpty(ImportPath)) - { - Console.WriteLine("Error: No import path specified! (--path)"); - return; - } - - string[] sourceFiles = ICommand.GetFilePathsInDirectory(ImportPath, ICommand.TargetFileType.Header, Recursive); - - if (sourceFiles.Length == 0) - { - Console.WriteLine($"Error: No valid file(s) found at the specified path ({ImportPath}). Ensure the path exists and spaces are properly enclosed. (--path)"); - return; - } - - List tasks = new List(); - foreach (string sourceFile in sourceFiles) - { - tasks.Add(Task.Run(async () => - { - FileAttributes fileAttributes; - try - { - fileAttributes = File.GetAttributes(ImportPath); - } - catch (FileNotFoundException) - { - Console.WriteLine("Error: Invalid file import path specified!"); - return; - } - catch (DirectoryNotFoundException) - { - Console.WriteLine("Error: Can not find directory for specified import path!"); - return; - } - catch (Exception e) - { - Console.WriteLine($"Error: Caught file exception: {e.Message}"); - return; - } - - var serializer = new SerializerBuilder() - .DisableAliases() - .WithTypeInspector(inner => new IncludeFieldsTypeInspector(inner)) - .WithTypeConverter(new ResourceYamlTypeConverter()) - .WithTypeConverter(new StrongIDYamlTypeConverter()) - .WithTypeConverter(new StringEnumYamlTypeConverter()) - .Build(); - - var serializedString = new string(""); - - bool isX64 = Format.EndsWith("x64", StringComparison.OrdinalIgnoreCase); - if (isX64) - Format = Format[..^3]; - - if (!TypeUtilities.TryParseEnum(Format, out Platform platform)) - { - throw new InvalidPlatformException("Error: Invalid file format specified!"); + public static string CommandToken => "ImportResource"; + public static string CommandDescription => "Imports information and relevant data from a specified platform's resource into a standardized format."; + public static string CommandParameters => "--recurse --overwrite --type= --format= --path="; + + public string? ResType { get; set; } + public string? Format { get; set; } + public string? ImportPath { get; set; } + public bool Overwrite { get; set; } + public bool Recursive { get; set; } + + public async Task Execute() + { + if (ResType == "AUTO") + { + Console.WriteLine("Error: Automatic typing is not supported yet! Please specify a type (--type)"); + return; } - if (!TypeUtilities.TryParseEnum(ResType, out ResourceType resType)) - { - Console.WriteLine("Error: Invalid resource type specified!"); - return; + if (string.IsNullOrEmpty(ImportPath)) + { + Console.WriteLine("Error: No import path specified! (--path)"); + return; } - Resource resource = ResourceFactory.CreateResource(resType, platform, sourceFile, isX64); + try + { + File.GetAttributes(ImportPath); + } + catch (FileNotFoundException) + { + Console.WriteLine("Error: Invalid file import path specified!"); + return; + } + catch (DirectoryNotFoundException) + { + Console.WriteLine("Error: Can not find directory for specified import path!"); + return; + } + catch (Exception e) + { + Console.WriteLine($"Error: Caught file exception: {e.Message}"); + return; + } - var resourceClass = resource.GetType(); - var resourceType = resource.GetResourceType(); + string[] sourceFiles = ICommand.GetFilePathsInDirectory(ImportPath, ICommand.TargetFileType.Header, Recursive); - string filePath = Path.Combine - ( - GetEnvironmentDirectory(EnvironmentDirectory.Resources), - $"{DBToFileRegex().Replace(resource.AssetName, "")}.{resourceType}" - ); + if (sourceFiles.Length == 0) + { + Console.WriteLine($"Error: No valid file(s) found at the specified path ({ImportPath}). Ensure the path exists and spaces are properly enclosed. (--path)"); + return; + } - string? directoryPath = Path.GetDirectoryName(filePath); + string formatValue = Format ?? string.Empty; + bool isX64 = formatValue.EndsWith("x64", StringComparison.OrdinalIgnoreCase); + if (isX64) + formatValue = formatValue[..^3]; - Directory.CreateDirectory(directoryPath); + if (!TypeUtilities.TryParseEnum(formatValue, out Platform platform)) + { + throw new InvalidPlatformException("Error: Invalid file format specified!"); + } - serializedString = serializer.Serialize(resource); - using (StreamWriter streamWriter = new StreamWriter(filePath)) - { - await streamWriter.WriteAsync(serializedString); - }; + if (!TypeUtilities.TryParseEnum(ResType, out ResourceType resType)) + { + Console.WriteLine("Error: Invalid resource type specified!"); + return; + } - // Texture-specific logic. Will need to refactor this pipeline - if (resourceType == ResourceType.Texture) - { - string texturePath = Path.Combine - ( - Path.GetDirectoryName(sourceFile), - Path.GetFileNameWithoutExtension(sourceFile) + - // TODO: Resource-defined Secondary path support - resource.Unpacker switch - { - Unpacker.Bnd2Manager => "_2.bin", - Unpacker.DGI => "_texture.dat", - Unpacker.YAP => "_secondary.dat", - Unpacker.Raw => "_texture.dat", // Fallback for now - Unpacker.Volatility => throw new NotImplementedException(), - _ => throw new NotImplementedException(), - } - ); - - if (File.Exists(texturePath)) - { - string outPath = Path.Combine - ( - directoryPath, - Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(Path.GetFullPath(filePath))) - ); - - File.Copy(texturePath, $"{outPath}.{resourceType}Bitmap", Overwrite); - } - } - - // Splicer-specific logic. Will need to refactor this pipeline - if (resourceType == ResourceType.Splicer) + string resourcesDirectory = GetEnvironmentDirectory(EnvironmentDirectory.Resources); + string toolsDirectory = GetEnvironmentDirectory(EnvironmentDirectory.Tools); + string splicerDirectory = GetEnvironmentDirectory(EnvironmentDirectory.Splicer); + + var importOperation = new ImportResourceOperation(resourcesDirectory, toolsDirectory, splicerDirectory, Overwrite); + var saveOperation = new SaveResourceOperation(); + + List tasks = new List(); + foreach (string sourceFile in sourceFiles) { - string sxPath = Path.Combine - ( - GetEnvironmentDirectory(EnvironmentDirectory.Tools), - "sx.exe" - ); - - bool sxExists = File.Exists(sxPath); - - Splicer? splicer = resource as Splicer; - - List? samples = splicer?.GetLoadedSamples(); - - string sampleDirectory = Path.Combine - ( - GetEnvironmentDirectory(EnvironmentDirectory.Splicer), - "Samples" - ); - - Directory.CreateDirectory(sampleDirectory); - - for (int i = 0; i < samples?.Count; i++) - { - string sampleName = $"{samples[i].SampleID}"; - - string samplePathName = Path.Combine(sampleDirectory, sampleName); - - if (!File.Exists($"{samplePathName}.snr") || Overwrite) - { - Console.WriteLine($"Writing extracted sample {sampleName}.snr"); - await File.WriteAllBytesAsync($"{samplePathName}.snr", samples[i].Data); - } - else - { - Console.WriteLine($"Skipping extracted sample {sampleName}.snr"); - } - - if (sxExists) - { - string convertedSamplePathName = Path.Combine(sampleDirectory, "_extracted"); - - Directory.CreateDirectory(convertedSamplePathName); - - convertedSamplePathName = Path.Combine(convertedSamplePathName, sampleName + ".wav"); - - if (!File.Exists(convertedSamplePathName) || Overwrite) - { - ProcessStartInfo start = new ProcessStartInfo - { - FileName = sxPath, - Arguments = $"-wave -s16l_int -v0 \"{samplePathName}.snr\" -=\"{convertedSamplePathName}\"", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - using (Process process = new Process()) - { - process.StartInfo = start; - process.OutputDataReceived += (sender, e) => - { - if (!string.IsNullOrEmpty(e.Data)) Console.WriteLine(e.Data); - }; - - process.ErrorDataReceived += (sender, e) => - { - if (!string.IsNullOrEmpty(e.Data)) Console.WriteLine(e.Data); - }; - - Console.WriteLine($"Converting extracted sample {sampleName}.snr to wave..."); - process.Start(); - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - process.WaitForExit(); - } - } - else - { - Console.WriteLine($"Converted sample {Path.GetFileName(convertedSamplePathName)} already exists, skipping..."); - } - } - } + tasks.Add(Task.Run(async () => + { + ImportResourceResult result = await importOperation.ExecuteAsync(resType, platform, sourceFile, isX64); + await saveOperation.ExecuteAsync(result.Resource, result.ResourcePath); + Console.WriteLine($"Imported {Path.GetFileName(sourceFile)} as {Path.GetFullPath(result.ResourcePath)}."); + })); } - Console.WriteLine($"Imported {Path.GetFileName(sourceFile)} as {Path.GetFullPath(filePath)}."); - })); - } - await Task.WhenAll(tasks); - } - - [GeneratedRegex(@"(\?ID=\d+)|:")] - private static partial Regex DBToFileRegex(); - - public void SetArgs(Dictionary args) - { - ResType = (args.TryGetValue("type", out object? restype) ? restype as string : "auto")?.ToUpper(); - Format = (args.TryGetValue("format", out object? format) ? format as string : "auto")?.ToUpper(); - ImportPath = args.TryGetValue("path", out object? path) ? path as string : ""; - Overwrite = args.TryGetValue("overwrite", out var ow) && (bool)ow; - Recursive = args.TryGetValue("recurse", out var re) && (bool)re; - } + + await Task.WhenAll(tasks); + } + + public void SetArgs(Dictionary args) + { + ResType = (args.TryGetValue("type", out object? restype) ? restype as string : "auto")?.ToUpper(); + Format = (args.TryGetValue("format", out object? format) ? format as string : "auto")?.ToUpper(); + ImportPath = args.TryGetValue("path", out object? path) ? path as string : ""; + Overwrite = args.TryGetValue("overwrite", out var ow) && (bool)ow; + Recursive = args.TryGetValue("recurse", out var re) && (bool)re; + } public ImportResourceCommand() { } -} \ No newline at end of file +} diff --git a/Volatility/CLI/Commands/ImportStringTableCommand.cs b/Volatility/CLI/Commands/ImportStringTableCommand.cs index 9ccac04..15eba6e 100644 --- a/Volatility/CLI/Commands/ImportStringTableCommand.cs +++ b/Volatility/CLI/Commands/ImportStringTableCommand.cs @@ -1,12 +1,6 @@ -using System.Text; -using System.Xml.Linq; - -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; +using Volatility.Operations.StringTables; using static Volatility.Utilities.EnvironmentUtilities; -using static Volatility.Utilities.ResourceIDUtilities; -using static Volatility.Utilities.DictUtilities; namespace Volatility.CLI.Commands; @@ -22,12 +16,6 @@ internal class ImportStringTableCommand : ICommand public bool Recursive { get; set; } public bool Verbose { get; set; } - private class ResourceEntry - { - public string Name { get; set; } = ""; - public List Appearances { get; set; } = new(); - } - public async Task Execute() { if (string.IsNullOrEmpty(ImportPath)) @@ -43,55 +31,19 @@ public async Task Execute() return; } - Console.WriteLine($"Importing data from ResourceStringTables into the ResourceDB... this may take a while!"); + Console.WriteLine("Importing data from ResourceStringTables into the ResourceDB... this may take a while!"); string directoryPath = GetEnvironmentDirectory(EnvironmentDirectory.ResourceDB); Directory.CreateDirectory(directoryPath); string yamlFile = Path.Combine(directoryPath, "ResourceDB.yaml"); - var deserializer = new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .Build(); - - var allEntries = File.Exists(yamlFile) - ? deserializer.Deserialize>>(await File.ReadAllTextAsync(yamlFile)) - ?? new Dictionary>(StringComparer.OrdinalIgnoreCase) - : new Dictionary>(StringComparer.OrdinalIgnoreCase); + var loadOperation = new LoadResourceDictionaryOperation(); + var mergeOperation = new MergeStringTableEntriesOperation(); + var importOperation = new ImportStringTableOperation(mergeOperation); - var results = await Task.WhenAll(filePaths.Select(ProcessFileAsync)); - foreach (var fileResult in results) - { - foreach (var typePair in fileResult) - { - var typeDict = allEntries.GetOrCreate(typePair.Key, () => new Dictionary()); - foreach (var resPair in typePair.Value) - { - if (!typeDict.TryGetValue(resPair.Key, out var existing)) - { - typeDict[resPair.Key] = resPair.Value; - } - else - { - if (Overwrite) - existing.Name = resPair.Value.Name; - foreach (var fn in resPair.Value.Appearances) - if (!existing.Appearances.Contains(fn)) - existing.Appearances.Add(fn); - } - } - } - } - - var serializer = new SerializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .Build(); + var allEntries = await loadOperation.ExecuteAsync(yamlFile); - string yaml = serializer.Serialize(allEntries); - await File.WriteAllTextAsync(yamlFile, yaml, Encoding.UTF8); - - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); + await importOperation.ExecuteAsync(filePaths, allEntries, Endian ?? "le", Overwrite, Verbose, yamlFile); Console.WriteLine($"Finished importing all ResourceDB (v2) data at {yamlFile}."); } @@ -105,50 +57,5 @@ public void SetArgs(Dictionary args) Verbose = args.TryGetValue("verbose", out var ve) && (bool)ve; } - private async Task>> ProcessFileAsync(string filePath) - { - var entriesByType = new Dictionary>(StringComparer.OrdinalIgnoreCase); - var fileName = Path.GetFileName(filePath)!; - var text = Encoding.UTF8.GetString(await File.ReadAllBytesAsync(filePath)); - - int start = text.IndexOf(""); - int end = text.IndexOf("") + "".Length; - if (start < 0 || end <= start) - { - if (Verbose) Console.WriteLine($"Skipping (no table): {fileName}"); - return entriesByType; - } - - XDocument xmlDoc = XDocument.Parse(text[start..end]); - var entries = xmlDoc.Descendants("Resource") - .Select(x => new - { - Id = Endian == "be" - ? FlipResourceIDEndian((string)x.Attribute("id")!) - : (string)x.Attribute("id")!, - Type = (string)x.Attribute("type")!, - Name = (string)x.Attribute("name")! - }).ToList(); - - foreach (var e in entries) - { - var dict = entriesByType.GetOrCreate(e.Type, () => new Dictionary()); - if (!dict.TryGetValue(e.Id, out var existing)) - { - dict[e.Id] = new ResourceEntry { Name = e.Name, Appearances = { fileName } }; - if (Verbose) Console.WriteLine($"Found {e.Type} entry in {Path.GetFileName(filePath)} - {e.Name}"); - } - else - { - if (Overwrite) - existing.Name = e.Name; - if (!existing.Appearances.Contains(fileName)) - existing.Appearances.Add(fileName); - } - } - - return entriesByType; - } - public ImportStringTableCommand() { } } diff --git a/Volatility/CLI/Commands/PortTextureCommand.cs b/Volatility/CLI/Commands/PortTextureCommand.cs index 9a21abb..bbc5b7c 100644 --- a/Volatility/CLI/Commands/PortTextureCommand.cs +++ b/Volatility/CLI/Commands/PortTextureCommand.cs @@ -1,8 +1,4 @@ -using System.Reflection; -using Volatility.Resources; -using Volatility.Utilities; - -using static Volatility.Utilities.ResourceIDUtilities; +using Volatility.Operations.Resources; namespace Volatility.CLI.Commands; @@ -28,307 +24,14 @@ public async Task Execute() } var sourceFiles = ICommand.GetFilePathsInDirectory(SourcePath, ICommand.TargetFileType.Header); - List tasks = new List(); Console.ForegroundColor = ConsoleColor.Blue; Console.WriteLine($"Starting {sourceFiles.Length} PortTexture tasks..."); Console.ResetColor(); - foreach (string sourceFile in sourceFiles) - { - tasks.Add(Task.Run(async () => - { - TextureBase? SourceTexture = ConstructHeader(sourceFile, SourceFormat, Verbose); - TextureBase? DestinationTexture = ConstructHeader(DestinationPath, DestinationFormat, Verbose); - - if (SourceTexture == null || DestinationTexture == null) - { - throw new InvalidOperationException("Failed to initialize texture header. Ensure the platform matches the file format and that the path is correct."); - } - - // TODO: Cleanup!! - SourceFormat = BPRx64Hack(SourceTexture, SourceFormat); - DestinationFormat = BPRx64Hack(DestinationTexture, DestinationFormat); - - SourceTexture.PullAll(); - - CopyProperties(SourceTexture, DestinationTexture); - - // Manual header format conversion - bool flipEndian = false; - int sourceFormatIndex = 0; - int destinationFormatIndex = 0; - switch ((SourceTexture, DestinationTexture)) - { - // ==== Console <> Console (no endian flip) - - case (TexturePS3 ps3, TextureX360 x360): - PS3toX360Mapping.TryGetValue(ps3.Format, out GPUTEXTUREFORMAT ps3x360Format); - x360.Format.DataFormat = ps3x360Format; - x360.Format.Endian = GPUENDIAN.GPUENDIAN_NONE; // This may need to be the default value for new 360 textures - flipEndian = false; - sourceFormatIndex = (int)ps3.Format; - destinationFormatIndex = (int)ps3x360Format; - if (ps3x360Format == GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_1_REVERSE) - Console.WriteLine($"WARNING: Destination texture format is {ps3x360Format}! (Source is {ps3.Format})"); - break; - case (TextureX360 x360, TexturePS3 ps3): - X360toPS3Mapping.TryGetValue(x360.Format.DataFormat, out CELL_GCM_COLOR_FORMAT x360ps3Format); - ps3.Format = x360ps3Format; - flipEndian = false; - sourceFormatIndex = (int)x360.Format.DataFormat; - destinationFormatIndex = (int)x360ps3Format; - if (x360ps3Format == CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_INVALID) - Console.WriteLine($"WARNING: Destination texture format is {x360ps3Format}! (Source is {x360.Format.DataFormat})"); - break; - - // ==== PC/BPR <> PC/BPR (no endian flip) - - case (TextureBPR bprsrc, TextureBPR bprdst): - // I don't know why this is required as base class format should be copied anyway. - bprdst.Format = bprsrc.Format; - sourceFormatIndex = (int)bprsrc.Format; - destinationFormatIndex = sourceFormatIndex; - break; - case (TexturePC tub, TextureBPR bpr): - TUBtoBPRMapping.TryGetValue(tub.Format, out DXGI_FORMAT tubbprFormat); - bpr.Format = tubbprFormat; - sourceFormatIndex = (int)tub.Format; - destinationFormatIndex = (int)tubbprFormat; - if (tubbprFormat == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN) - Console.WriteLine($"WARNING: Destination texture format is {tubbprFormat}! (Source is {tub.Format})"); - break; - case (TextureBPR bpr, TexturePC tub): - BPRtoTUBMapping.TryGetValue(bpr.Format, out D3DFORMAT bprtubFormat); - tub.Format = bprtubFormat; - sourceFormatIndex = (int)bpr.Format; - destinationFormatIndex = (int)bprtubFormat; - if (bprtubFormat == D3DFORMAT.D3DFMT_UNKNOWN) - Console.WriteLine($"WARNING: Destination texture format is {bprtubFormat}! (Source is {bpr.Format})"); - break; - - // ==== Console <> PC/BPR (endian flip) - - // = PS3 Source - case (TexturePS3 ps3, TextureBPR bpr): - PS3toBPRMapping.TryGetValue(ps3.Format, out DXGI_FORMAT ps3bprFormat); - bpr.Format = ps3bprFormat; - flipEndian = true; - sourceFormatIndex = (int)ps3.Format; - destinationFormatIndex = (int)ps3bprFormat; - if (ps3bprFormat == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN) - Console.WriteLine($"WARNING: Destination texture format is {ps3bprFormat}! (Source is {ps3.Format})"); - break; - case (TexturePS3 ps3, TexturePC tub): - PS3toTUBMapping.TryGetValue(ps3.Format, out D3DFORMAT ps3tubFormat); - tub.Format = ps3tubFormat; - flipEndian = true; - sourceFormatIndex = (int)ps3.Format; - destinationFormatIndex = (int)ps3tubFormat; - if (ps3tubFormat == D3DFORMAT.D3DFMT_UNKNOWN) - Console.WriteLine($"WARNING: Destination texture format is {ps3tubFormat}! (Source is {ps3.Format})"); - break; - - // = X360 Source - case (TextureX360 x360, TexturePC tub): - X360toTUBMapping.TryGetValue(x360.Format.DataFormat, out D3DFORMAT x360tubFormat); - tub.Format = x360tubFormat; - flipEndian = true; - sourceFormatIndex = (int)x360.Format.DataFormat; - destinationFormatIndex = (int)x360tubFormat; - if (x360tubFormat == D3DFORMAT.D3DFMT_UNKNOWN) - Console.WriteLine($"WARNING: Destination texture format is {x360tubFormat}! (Source is {x360.Format.DataFormat})"); - break; - case (TextureX360 x360, TextureBPR bpr): - X360toBPRMapping.TryGetValue(x360.Format.DataFormat, out DXGI_FORMAT x360bprFormat); - bpr.Format = x360bprFormat; - flipEndian = true; - sourceFormatIndex = (int)x360.Format.DataFormat; - destinationFormatIndex = (int)x360bprFormat; - if (x360bprFormat == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN) - Console.WriteLine($"WARNING: Destination texture format is {x360bprFormat}! (Source is {x360.Format.DataFormat})"); - break; - - // = TUB Source - case (TexturePC tub, TextureX360 x360): - TUBtoX360Mapping.TryGetValue(tub.Format, out GPUTEXTUREFORMAT tubx360Format); - x360.Format.DataFormat = tubx360Format; - flipEndian = true; - sourceFormatIndex = (int)tub.Format; - destinationFormatIndex = (int)tubx360Format; - if (tubx360Format == GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_1_REVERSE) - Console.WriteLine($"WARNING: Destination texture format is {tubx360Format}! (Source is {tub.Format})"); - break; - - // = BPR Source - case (TextureBPR bpr, TexturePS3 ps3): - BPRtoPS3Mapping.TryGetValue(bpr.Format, out CELL_GCM_COLOR_FORMAT bprps3format); - ps3.Format = bprps3format; - flipEndian = true; - sourceFormatIndex = (int)bpr.Format; - destinationFormatIndex = (int)bprps3format; - if (bprps3format == CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_INVALID) - Console.WriteLine($"WARNING: Destination texture format is {bprps3format}! (Source is {bpr.Format})"); - break; - case (TextureBPR bpr, TextureX360 x360): - BPRtoX360Mapping.TryGetValue(bpr.Format, out GPUTEXTUREFORMAT bprx360Format); - x360.Format.DataFormat = bprx360Format; - flipEndian = true; - sourceFormatIndex = (int)bpr.Format; - destinationFormatIndex = (int)bprx360Format; - if (bprx360Format == GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_1_REVERSE) - Console.WriteLine($"WARNING: Destination texture format is {bprx360Format}! (Source is {bpr.Format})"); - break; - - default: - throw new NotImplementedException($"Conversion technique {SourceFormat} > {DestinationFormat} is not yet implemented."); - }; - - // Finalize Destination - DestinationTexture.PushAll(); - - string outPath = @""; - - string outResourceFilename = (flipEndian && SourceTexture.Unpacker != Unpacker.YAP) - ? FlipPathResourceIDEndian(Path.GetFileName(sourceFile)) - : Path.GetFileName(sourceFile); - - if (DestinationPath == sourceFile) - { - outPath = $"{Path.GetDirectoryName(DestinationPath)}{Path.DirectorySeparatorChar}{outResourceFilename}"; - } - // If we're outputting to a directory - else if (new DirectoryInfo(DestinationPath).Exists) - { - outPath = DestinationPath + Path.DirectorySeparatorChar + outResourceFilename; - } - - // TODO: Resource-defined Secondary path support - string secondaryExtension = SourceTexture.Unpacker switch - { - Unpacker.Bnd2Manager => "_2.bin", - Unpacker.DGI => "_texture.dat", - Unpacker.YAP => "_secondary.dat", - Unpacker.Raw => "_texture.dat", // Fallback for now - Unpacker.Volatility => throw new NotImplementedException(), - _ => throw new NotImplementedException(), - }; - - string primaryExtension = SourceTexture.Unpacker switch - { - Unpacker.Bnd2Manager => "_1.bin", - Unpacker.DGI => ".dat", - Unpacker.YAP => "_primary.dat", - Unpacker.Raw => ".dat", // Fallback for now - Unpacker.Volatility => throw new NotImplementedException(), - _ => throw new NotImplementedException(), - }; + var operation = new PortTextureOperation(); - string sourceBitmapPath = $"{Path.GetDirectoryName(sourceFile)}{Path.DirectorySeparatorChar}{Path.GetFileName(sourceFile).Split(primaryExtension)[0]}{secondaryExtension}"; - - if (!Path.Exists(sourceBitmapPath)) - { - Console.WriteLine($"Failed to find associated bitmap data for {Path.GetFileNameWithoutExtension(sourceFile)} at path {sourceBitmapPath}!"); - } - - string destinationBitmapPath = $"{Path.GetDirectoryName(outPath)}{Path.DirectorySeparatorChar}{Path.GetFileName(sourceFile).Split(primaryExtension)[0]}{secondaryExtension}"; - - if (Path.Exists(destinationBitmapPath)) - { - if (Verbose) Console.WriteLine($"Found existing bitmap data at {destinationBitmapPath}, overwriting..."); - } - - try - { - // Currently requires an external tool. Every texture I've encountered on PS3 is - // already raw DDS anyway, so there's not really any reason to do this as far as I see. - - if (UseGTF && SourceTexture.GetResourcePlatform() == Platform.PS3) - { - PS3TextureUtilities.PS3GTFToDDS(SourcePath, sourceBitmapPath, destinationBitmapPath, Verbose); - } - - if (DestinationTexture is TextureX360 destX && SourceTexture.GetResourcePlatform() != Platform.X360) - { - destX.Format.MaxMipLevel = destX.Format.MinMipLevel; - - //if (DestinationTexture.MipmapLevels > 0) - //{ - // // - Repack Mipmaps (WIP!) - // try - // { - // if (Verbose) Console.WriteLine($"Converting mipmap data to X360 format for {Path.GetDirectoryName(outPath)}{Path.DirectorySeparatorChar}{Path.GetFileNameWithoutExtension(outPath)}_texture.dat..."); - // X360TextureUtilities.ConvertMipmapsToX360(destX, destX.Format.DataFormat, sourceBitmapPath, destinationBitmapPath); - // } - // catch (Exception e) - // { - // Console.WriteLine($"Error converting mipmap data to X360 format for {Path.GetFileNameWithoutExtension(sourceFile)}: {e.Message}"); - // } - //} - } - if (SourceTexture is TextureX360 sourceX) - { - if (sourceX.Format.Tiled && !string.IsNullOrEmpty(sourceBitmapPath)) - { - if (Verbose) Console.WriteLine($"Detiling X360 bitmap data for {Path.GetDirectoryName(outPath)}{Path.DirectorySeparatorChar}{Path.GetFileNameWithoutExtension(outPath)}_texture.dat..."); - X360TextureUtilities.WriteUntiled360TextureFile(sourceX, sourceBitmapPath, destinationBitmapPath); - } - } - else - { - - if (!TryConvertTexture( - SourceTexture, - DestinationTexture, - sourceBitmapPath, - destinationBitmapPath - ) - ) - { - if (Verbose) Console.WriteLine($"Copying associated bitmap data for {Path.GetDirectoryName(outPath)}{Path.DirectorySeparatorChar}{Path.GetFileNameWithoutExtension(outPath)}_texture.dat..."); - File.Copy(sourceBitmapPath, destinationBitmapPath, true); - } - else - { - if (Verbose) Console.WriteLine($"Converting associated bitmap data for {Path.GetDirectoryName(outPath)}{Path.DirectorySeparatorChar}{Path.GetFileNameWithoutExtension(outPath)}_texture.dat..."); - } - } - if (Verbose) Console.WriteLine($"Wrote texture bitmap data to {DestinationFormat} destination directory."); - - // Set ContentsSize for BPR textures if applicable. - if (DestinationTexture is TextureBPR destBPRTexture && File.Exists(destinationBitmapPath)) - { - destBPRTexture.PlacedDataSize = (uint)new FileInfo(destinationBitmapPath).Length; - if (Verbose) Console.WriteLine($"BPR PlacedDataSize set to {destBPRTexture.PlacedDataSize} (file: {destinationBitmapPath})."); - } - - // Write header data (now after bitmap data to ensure any final edits are included) - using FileStream fs = new(outPath, FileMode.Create, FileAccess.Write); - using (EndianAwareBinaryWriter writer = new(fs, DestinationTexture.GetResourceEndian())) - { - try - { - if (Verbose) Console.WriteLine($"Writing converted {DestinationFormat} texture property data to destination file {Path.GetFileName(outPath)}..."); - DestinationTexture.WriteToStream(writer); - } - catch - { - throw new IOException("Failed to write converted texture property data to stream."); - } - writer.Close(); - fs.Close(); - } - } - catch (Exception ex) - { - Console.WriteLine($"Error trying to copy bitmap data for {Path.GetFileNameWithoutExtension(sourceFile)}: {ex.Message}"); - } - - Console.WriteLine($"Successfully ported {SourceFormat} formatted {Path.GetFileNameWithoutExtension(sourceFile)} to {DestinationFormat} as {Path.GetFileNameWithoutExtension(outPath)}."); - })); - } - - await Task.WhenAll(tasks); + await operation.ExecuteAsync(sourceFiles, SourceFormat ?? string.Empty, SourcePath ?? string.Empty, DestinationFormat ?? string.Empty, DestinationPath, Verbose, UseGTF); } public void SetArgs(Dictionary args) @@ -342,197 +45,12 @@ public void SetArgs(Dictionary args) DestinationFormat = (args.TryGetValue("outformat", out object? outformat) ? outformat as string : args.TryGetValue("of", out object? off) ? off as string : "auto").ToUpper(); - DestinationPath = args.TryGetValue("outpath", out object? outpath) ? inpath as string + DestinationPath = args.TryGetValue("outpath", out object? outpath) ? outpath as string : args.TryGetValue("op", out object? opp) ? opp as string : SourcePath; Verbose = args.TryGetValue("verbose", out var verbose) && (bool)verbose; UseGTF = args.TryGetValue("usegtf", out var usegtf) && (bool)usegtf; } - public string BPRx64Hack(TextureBase header, string format) - { - if (header.GetResourcePlatform() == Platform.BPR && format.EndsWith("X64")) - { - header.SetResourceArch(Arch.x64); - return "BPR"; - } - return format; - } - - public static TextureBase? ConstructHeader(string Path, string Format, bool Verbose = true) - { - // TODO: set x64 bool when constructing x64 - if (Verbose) Console.WriteLine($"Constructing {Format} texture property data..."); - return Format switch - { - "BPR" => new TextureBPR(Path), - "BPRX64" => new TextureBPR(Path), - "TUB" => new TexturePC(Path), - "X360" => new TextureX360(Path), - "PS3" => new TexturePS3(Path), - _ => throw new InvalidPlatformException(), - }; - } - - public static void CopyProperties(TextureBase source, TextureBase destination) - { - if (source == null) throw new ArgumentNullException(nameof(source)); - if (destination == null) throw new ArgumentNullException(nameof(destination)); - - var srcType = source.GetType(); - var dstType = destination.GetType(); - - var typeToReflect = srcType == dstType - ? srcType - : typeof(TextureBase); - - var props = typeToReflect - .GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(p => p.CanRead - && p.CanWrite - && p.GetIndexParameters().Length == 0); - - foreach (var prop in props) - { - var value = prop.GetValue(source); - prop.SetValue(destination, value); - } - } - - public bool TryConvertTexture(TextureBase srcTexture, TextureBase destTexture, string inPath, string outPath) - { - byte[] bitmap = File.ReadAllBytes(inPath); - switch (srcTexture, destTexture) - { - case (TexturePS3 ps3, TextureBPR bpr): - if (ps3.Format == CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_A8R8G8B8 - && bpr.Format == DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM) - DDSTextureUtilities.A8R8G8B8toB8G8R8A8(bitmap, destTexture.Width, destTexture.Height, destTexture.MipmapLevels); - break; - case (TexturePC tub, TextureBPR bpr): - if (tub.Format == D3DFORMAT.D3DFMT_A8R8G8B8 - && bpr.Format == DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM) - DDSTextureUtilities.A8R8G8B8toB8G8R8A8(bitmap, destTexture.Width, destTexture.Height, destTexture.MipmapLevels); - if (tub.Format == D3DFORMAT.D3DFMT_A8B8G8R8 - && bpr.Format == DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM) - DDSTextureUtilities.A8B8G8R8toB8G8R8A8(bitmap, destTexture.Width, destTexture.Height, destTexture.MipmapLevels); - break; - default: - bitmap = []; - return false; - }; - File.WriteAllBytes(outPath, bitmap); - return true; - } - - private static readonly Dictionary X360toTUBMapping = new() - { - { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT1, D3DFORMAT.D3DFMT_DXT1 }, - { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT2_3, D3DFORMAT.D3DFMT_DXT3 }, - { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT4_5, D3DFORMAT.D3DFMT_DXT5 }, - { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_8, D3DFORMAT.D3DFMT_A8 }, - { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_16_16_16_16, D3DFORMAT.D3DFMT_A16B16G16R16 }, - { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_8_8_8_8, D3DFORMAT.D3DFMT_A8R8G8B8 }, - // TODO: Add more mappings - }; - - private static readonly Dictionary X360toBPRMapping = new() - { - { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT1, DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM }, - { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT2_3, DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM }, - { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT4_5, DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM }, - { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_8, DXGI_FORMAT.DXGI_FORMAT_A8_UNORM }, - { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_16_16_16_16, DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM }, - { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_8_8_8_8, DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM }, - // TODO: Add more mappings - }; - - private static readonly Dictionary TUBtoBPRMapping = new() - { - { D3DFORMAT.D3DFMT_DXT1, DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM }, - { D3DFORMAT.D3DFMT_DXT3, DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM }, - { D3DFORMAT.D3DFMT_DXT5, DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM }, - { D3DFORMAT.D3DFMT_A8, DXGI_FORMAT.DXGI_FORMAT_A8_UNORM }, - { D3DFORMAT.D3DFMT_A8B8G8R8, DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM }, // Does not directly match without converting! - { D3DFORMAT.D3DFMT_A8R8G8B8, DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM }, // Does not directly match without converting! - // TODO: Add more mappings - }; - - private static readonly Dictionary BPRtoTUBMapping = new() - { - { DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM, D3DFORMAT.D3DFMT_DXT1 }, - { DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM, D3DFORMAT.D3DFMT_DXT3 }, - { DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM, D3DFORMAT.D3DFMT_DXT5 }, - { DXGI_FORMAT.DXGI_FORMAT_A8_UNORM, D3DFORMAT.D3DFMT_A8 }, - { DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM, D3DFORMAT.D3DFMT_A8B8G8R8 }, // Does not directly match without converting! - // TODO: Add more mappings - }; - - private static readonly Dictionary PS3toTUBMapping = new() - { - { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT1, D3DFORMAT.D3DFMT_DXT1 }, - { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT23, D3DFORMAT.D3DFMT_DXT3 }, - { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT45, D3DFORMAT.D3DFMT_DXT5 }, - { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_B8, D3DFORMAT.D3DFMT_A8 }, - { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_A8R8G8B8, D3DFORMAT.D3DFMT_A8R8G8B8 }, - // TODO: Add more mappings - }; - - private static readonly Dictionary PS3toX360Mapping = new() - { - { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT1, GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT1 }, - { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT23, GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT2_3 }, - { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT45, GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT4_5 }, - { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_B8, GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_8_B }, - { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_A8R8G8B8, GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_8_8_8_8 }, - // TODO: Add more mappings - }; - - private static readonly Dictionary TUBtoX360Mapping = new() - { - { D3DFORMAT.D3DFMT_DXT1, GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT1 }, - { D3DFORMAT.D3DFMT_DXT3, GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT2_3 }, - { D3DFORMAT.D3DFMT_DXT5, GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT4_5 }, - // TODO: Add more mappings - }; - - private static readonly Dictionary X360toPS3Mapping = new() - { - { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT1, CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT1 }, - { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT2_3, CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT23 }, - { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT4_5, CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT45 }, - { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_8_B, CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_B8 }, - { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_8_8_8_8, CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_A8R8G8B8 }, - // TODO: Add more mappings - }; - - private static readonly Dictionary PS3toBPRMapping = new() - { - { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT1, DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM }, - { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT23, DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM }, - { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT45, DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM }, - { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_B8, DXGI_FORMAT.DXGI_FORMAT_A8_UNORM }, - { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_A8R8G8B8, DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM } - // TODO: Add more mappings - }; - - private static readonly Dictionary BPRtoPS3Mapping = new() - { - { DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM, CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT1 }, - { DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM, CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT23 }, - { DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM, CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT45 }, - { DXGI_FORMAT.DXGI_FORMAT_A8_UNORM, CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_B8 }, - // TODO: Add more mappings - }; - - private static readonly Dictionary BPRtoX360Mapping = new() - { - { DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM, GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT1 }, - { DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM, GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT2_3 }, - { DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM, GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT4_5 }, - // TODO: Add more mappings - }; - public PortTextureCommand() { } } - diff --git a/Volatility/Operations/Resources/ExportResourceOperation.cs b/Volatility/Operations/Resources/ExportResourceOperation.cs new file mode 100644 index 0000000..5f6e24c --- /dev/null +++ b/Volatility/Operations/Resources/ExportResourceOperation.cs @@ -0,0 +1,37 @@ +using Volatility.Resources; +using Volatility.Utilities; + +namespace Volatility.Operations.Resources; + +internal class ExportResourceOperation +{ + public Task ExecuteAsync(Resource resource, string outputPath, Platform platform) + { + string? directoryPath = Path.GetDirectoryName(outputPath); + + if (!string.IsNullOrEmpty(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + + using FileStream fs = new(outputPath, FileMode.Create); + + Endian endian = resource.GetResourceEndian() != Endian.Agnostic + ? resource.GetResourceEndian() + : EndianMapping.GetDefaultEndian(platform); + + using EndianAwareBinaryWriter writer = new(fs, endian); + + switch (resource) + { + case TextureBase texture: + texture.PushAll(); + goto default; + default: + resource.WriteToStream(writer); + break; + } + + return Task.CompletedTask; + } +} diff --git a/Volatility/Operations/Resources/ImportResourceOperation.cs b/Volatility/Operations/Resources/ImportResourceOperation.cs new file mode 100644 index 0000000..7d35b2e --- /dev/null +++ b/Volatility/Operations/Resources/ImportResourceOperation.cs @@ -0,0 +1,166 @@ +using System.Diagnostics; +using System.Text.RegularExpressions; + +using Volatility.Resources; +using Volatility.Utilities; + +using static Volatility.Utilities.EnvironmentUtilities; + +namespace Volatility.Operations.Resources; + +internal partial class ImportResourceOperation +{ + private readonly string resourcesDirectory; + private readonly string toolsDirectory; + private readonly string splicerDirectory; + private readonly bool overwrite; + + public ImportResourceOperation(string resourcesDirectory, string toolsDirectory, string splicerDirectory, bool overwrite) + { + this.resourcesDirectory = resourcesDirectory; + this.toolsDirectory = toolsDirectory; + this.splicerDirectory = splicerDirectory; + this.overwrite = overwrite; + } + + public async Task ExecuteAsync(ResourceType resourceType, Platform platform, string sourceFile, bool isX64) + { + Resource resource = ResourceFactory.CreateResource(resourceType, platform, sourceFile, isX64); + + string filePath = Path.Combine + ( + resourcesDirectory, + $"{DBToFileRegex().Replace(resource.AssetName, string.Empty)}.{resourceType}" + ); + + string? directoryPath = Path.GetDirectoryName(filePath); + + if (!string.IsNullOrEmpty(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + + if (resourceType == ResourceType.Texture) + { + string texturePath = Path.Combine + ( + Path.GetDirectoryName(sourceFile) ?? string.Empty, + Path.GetFileNameWithoutExtension(sourceFile) + + resource.Unpacker switch + { + Unpacker.Bnd2Manager => "_2.bin", + Unpacker.DGI => "_texture.dat", + Unpacker.YAP => "_secondary.dat", + Unpacker.Raw => "_texture.dat", + Unpacker.Volatility => throw new NotImplementedException(), + _ => throw new NotImplementedException(), + } + ); + + if (File.Exists(texturePath)) + { + string outPath = Path.Combine + ( + directoryPath ?? string.Empty, + Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(Path.GetFullPath(filePath))) + ); + + File.Copy(texturePath, $"{outPath}.{resourceType}Bitmap", overwrite); + } + } + + if (resourceType == ResourceType.Splicer) + { + string sxPath = Path.Combine + ( + toolsDirectory, + "sx.exe" + ); + + bool sxExists = File.Exists(sxPath); + + Splicer? splicer = resource as Splicer; + + List? samples = splicer?.GetLoadedSamples(); + + string sampleDirectory = Path.Combine + ( + splicerDirectory, + "Samples" + ); + + Directory.CreateDirectory(sampleDirectory); + + if (samples != null) + { + for (int i = 0; i < samples.Count; i++) + { + string sampleName = $"{samples[i].SampleID}"; + + string samplePathName = Path.Combine(sampleDirectory, sampleName); + + if (!File.Exists($"{samplePathName}.snr") || overwrite) + { + Console.WriteLine($"Writing extracted sample {sampleName}.snr"); + await File.WriteAllBytesAsync($"{samplePathName}.snr", samples[i].Data); + } + else + { + Console.WriteLine($"Skipping extracted sample {sampleName}.snr"); + } + + if (sxExists) + { + string convertedSamplePathName = Path.Combine(sampleDirectory, "_extracted"); + + Directory.CreateDirectory(convertedSamplePathName); + + convertedSamplePathName = Path.Combine(convertedSamplePathName, sampleName + ".wav"); + + if (!File.Exists(convertedSamplePathName) || overwrite) + { + ProcessStartInfo start = new ProcessStartInfo + { + FileName = sxPath, + Arguments = $"-wave -s16l_int -v0 \"{samplePathName}.snr\" -=\"{convertedSamplePathName}\"", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using Process process = new Process(); + process.StartInfo = start; + process.OutputDataReceived += (sender, e) => + { + if (!string.IsNullOrEmpty(e.Data)) Console.WriteLine(e.Data); + }; + + process.ErrorDataReceived += (sender, e) => + { + if (!string.IsNullOrEmpty(e.Data)) Console.WriteLine(e.Data); + }; + + Console.WriteLine($"Converting extracted sample {sampleName}.snr to wave..."); + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + process.WaitForExit(); + } + else + { + Console.WriteLine($"Converted sample {Path.GetFileName(convertedSamplePathName)} already exists, skipping..."); + } + } + } + } + } + + return new ImportResourceResult(resource, filePath); + } + + [GeneratedRegex(@"(\?ID=\d+)|:")] + private static partial Regex DBToFileRegex(); +} + +internal sealed record ImportResourceResult(Resource Resource, string ResourcePath); diff --git a/Volatility/Operations/Resources/LoadResourceOperation.cs b/Volatility/Operations/Resources/LoadResourceOperation.cs new file mode 100644 index 0000000..5979d26 --- /dev/null +++ b/Volatility/Operations/Resources/LoadResourceOperation.cs @@ -0,0 +1,25 @@ +using System.Runtime.Serialization; + +using Volatility.Resources; +using Volatility.Utilities; + +namespace Volatility.Operations.Resources; + +internal class LoadResourceOperation +{ + public async Task ExecuteAsync(string sourceFile, ResourceType resourceType, Platform platform) + { + string yaml = await File.ReadAllTextAsync(sourceFile); + + Resource resource = ResourceFactory.CreateResource(resourceType, platform, string.Empty); + + Resource? result = (Resource?)ResourceYamlDeserializer.DeserializeResource(resource.GetType(), yaml); + + if (result is null) + { + throw new SerializationException(); + } + + return result; + } +} diff --git a/Volatility/Operations/Resources/PortTextureOperation.cs b/Volatility/Operations/Resources/PortTextureOperation.cs new file mode 100644 index 0000000..72ca742 --- /dev/null +++ b/Volatility/Operations/Resources/PortTextureOperation.cs @@ -0,0 +1,438 @@ +using System.Reflection; + +using Volatility.Resources; +using Volatility.Utilities; + +using static Volatility.Utilities.ResourceIDUtilities; + +namespace Volatility.Operations.Resources; + +internal class PortTextureOperation +{ + public async Task ExecuteAsync(IEnumerable sourceFiles, string sourceFormat, string sourcePath, string destinationFormat, string? destinationPath, bool verbose, bool useGtf) + { + string resolvedDestinationPath = string.IsNullOrEmpty(destinationPath) ? sourcePath : destinationPath; + + List tasks = new List(); + + foreach (string sourceFile in sourceFiles) + { + tasks.Add(Task.Run(async () => + { + TextureBase? sourceTexture = ConstructHeader(sourceFile, sourceFormat, verbose); + TextureBase? destinationTexture = ConstructHeader(resolvedDestinationPath, destinationFormat, verbose); + + if (sourceTexture == null || destinationTexture == null) + { + throw new InvalidOperationException("Failed to initialize texture header. Ensure the platform matches the file format and that the path is correct."); + } + + string localSourceFormat = BPRx64Hack(sourceTexture, sourceFormat); + string localDestinationFormat = BPRx64Hack(destinationTexture, destinationFormat); + + sourceTexture.PullAll(); + + CopyProperties(sourceTexture, destinationTexture); + + bool flipEndian = false; + int sourceFormatIndex = 0; + int destinationFormatIndex = 0; + switch ((sourceTexture, destinationTexture)) + { + case (TexturePS3 ps3, TextureX360 x360): + PS3toX360Mapping.TryGetValue(ps3.Format, out GPUTEXTUREFORMAT ps3x360Format); + x360.Format.DataFormat = ps3x360Format; + x360.Format.Endian = GPUENDIAN.GPUENDIAN_NONE; + flipEndian = false; + sourceFormatIndex = (int)ps3.Format; + destinationFormatIndex = (int)ps3x360Format; + if (ps3x360Format == GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_1_REVERSE) + Console.WriteLine($"WARNING: Destination texture format is {ps3x360Format}! (Source is {ps3.Format})"); + break; + case (TextureX360 x360, TexturePS3 ps3): + X360toPS3Mapping.TryGetValue(x360.Format.DataFormat, out CELL_GCM_COLOR_FORMAT x360ps3Format); + ps3.Format = x360ps3Format; + flipEndian = false; + sourceFormatIndex = (int)x360.Format.DataFormat; + destinationFormatIndex = (int)x360ps3Format; + if (x360ps3Format == CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_INVALID) + Console.WriteLine($"WARNING: Destination texture format is {x360ps3Format}! (Source is {x360.Format.DataFormat})"); + break; + case (TextureBPR bprsrc, TextureBPR bprdst): + bprdst.Format = bprsrc.Format; + sourceFormatIndex = (int)bprsrc.Format; + destinationFormatIndex = sourceFormatIndex; + break; + case (TexturePC tub, TextureBPR bpr): + TUBtoBPRMapping.TryGetValue(tub.Format, out DXGI_FORMAT tubbprFormat); + bpr.Format = tubbprFormat; + sourceFormatIndex = (int)tub.Format; + destinationFormatIndex = (int)tubbprFormat; + if (tubbprFormat == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN) + Console.WriteLine($"WARNING: Destination texture format is {tubbprFormat}! (Source is {tub.Format})"); + break; + case (TextureBPR bpr, TexturePC tub): + BPRtoTUBMapping.TryGetValue(bpr.Format, out D3DFORMAT bprtubFormat); + tub.Format = bprtubFormat; + sourceFormatIndex = (int)bpr.Format; + destinationFormatIndex = (int)bprtubFormat; + if (bprtubFormat == D3DFORMAT.D3DFMT_UNKNOWN) + Console.WriteLine($"WARNING: Destination texture format is {bprtubFormat}! (Source is {bpr.Format})"); + break; + case (TexturePS3 ps3, TextureBPR bpr): + PS3toBPRMapping.TryGetValue(ps3.Format, out DXGI_FORMAT ps3bprFormat); + bpr.Format = ps3bprFormat; + flipEndian = true; + sourceFormatIndex = (int)ps3.Format; + destinationFormatIndex = (int)ps3bprFormat; + if (ps3bprFormat == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN) + Console.WriteLine($"WARNING: Destination texture format is {ps3bprFormat}! (Source is {ps3.Format})"); + break; + case (TexturePS3 ps3, TexturePC tub): + PS3toTUBMapping.TryGetValue(ps3.Format, out D3DFORMAT ps3tubFormat); + tub.Format = ps3tubFormat; + flipEndian = true; + sourceFormatIndex = (int)ps3.Format; + destinationFormatIndex = (int)ps3tubFormat; + if (ps3tubFormat == D3DFORMAT.D3DFMT_UNKNOWN) + Console.WriteLine($"WARNING: Destination texture format is {ps3tubFormat}! (Source is {ps3.Format})"); + break; + case (TextureX360 x360, TexturePC tub): + X360toTUBMapping.TryGetValue(x360.Format.DataFormat, out D3DFORMAT x360tubFormat); + tub.Format = x360tubFormat; + flipEndian = true; + sourceFormatIndex = (int)x360.Format.DataFormat; + destinationFormatIndex = (int)x360tubFormat; + if (x360tubFormat == D3DFORMAT.D3DFMT_UNKNOWN) + Console.WriteLine($"WARNING: Destination texture format is {x360tubFormat}! (Source is {x360.Format.DataFormat})"); + break; + case (TextureX360 x360, TextureBPR bpr): + X360toBPRMapping.TryGetValue(x360.Format.DataFormat, out DXGI_FORMAT x360bprFormat); + bpr.Format = x360bprFormat; + flipEndian = true; + sourceFormatIndex = (int)x360.Format.DataFormat; + destinationFormatIndex = (int)x360bprFormat; + if (x360bprFormat == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN) + Console.WriteLine($"WARNING: Destination texture format is {x360bprFormat}! (Source is {x360.Format.DataFormat})"); + break; + case (TexturePC tub, TextureX360 x360): + TUBtoX360Mapping.TryGetValue(tub.Format, out GPUTEXTUREFORMAT tubx360Format); + x360.Format.DataFormat = tubx360Format; + flipEndian = true; + sourceFormatIndex = (int)tub.Format; + destinationFormatIndex = (int)tubx360Format; + if (tubx360Format == GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_1_REVERSE) + Console.WriteLine($"WARNING: Destination texture format is {tubx360Format}! (Source is {tub.Format})"); + break; + case (TextureBPR bpr, TexturePS3 ps3): + BPRtoPS3Mapping.TryGetValue(bpr.Format, out CELL_GCM_COLOR_FORMAT bprps3format); + ps3.Format = bprps3format; + flipEndian = true; + sourceFormatIndex = (int)bpr.Format; + destinationFormatIndex = (int)bprps3format; + if (bprps3format == CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_INVALID) + Console.WriteLine($"WARNING: Destination texture format is {bprps3format}! (Source is {bpr.Format})"); + break; + case (TextureBPR bpr, TextureX360 x360): + BPRtoX360Mapping.TryGetValue(bpr.Format, out GPUTEXTUREFORMAT bprx360Format); + x360.Format.DataFormat = bprx360Format; + flipEndian = true; + sourceFormatIndex = (int)bpr.Format; + destinationFormatIndex = (int)bprx360Format; + if (bprx360Format == GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_1_REVERSE) + Console.WriteLine($"WARNING: Destination texture format is {bprx360Format}! (Source is {bpr.Format})"); + break; + default: + throw new NotImplementedException($"Conversion technique {localSourceFormat} > {localDestinationFormat} is not yet implemented."); + }; + + destinationTexture.PushAll(); + + string outPath = string.Empty; + + string outResourceFilename = (flipEndian && sourceTexture.Unpacker != Unpacker.YAP) + ? FlipPathResourceIDEndian(Path.GetFileName(sourceFile)) + : Path.GetFileName(sourceFile); + + if (resolvedDestinationPath == sourceFile) + { + outPath = $"{Path.GetDirectoryName(resolvedDestinationPath)}{Path.DirectorySeparatorChar}{outResourceFilename}"; + } + else if (new DirectoryInfo(resolvedDestinationPath).Exists) + { + outPath = resolvedDestinationPath + Path.DirectorySeparatorChar + outResourceFilename; + } + + string secondaryExtension = sourceTexture.Unpacker switch + { + Unpacker.Bnd2Manager => "_2.bin", + Unpacker.DGI => "_texture.dat", + Unpacker.YAP => "_secondary.dat", + Unpacker.Raw => "_texture.dat", + Unpacker.Volatility => throw new NotImplementedException(), + _ => throw new NotImplementedException(), + }; + + string primaryExtension = sourceTexture.Unpacker switch + { + Unpacker.Bnd2Manager => "_1.bin", + Unpacker.DGI => ".dat", + Unpacker.YAP => "_primary.dat", + Unpacker.Raw => ".dat", + Unpacker.Volatility => throw new NotImplementedException(), + _ => throw new NotImplementedException(), + }; + + string sourceBitmapPath = $"{Path.GetDirectoryName(sourceFile)}{Path.DirectorySeparatorChar}{Path.GetFileName(sourceFile).Split(primaryExtension)[0]}{secondaryExtension}"; + + if (!Path.Exists(sourceBitmapPath)) + { + Console.WriteLine($"Failed to find associated bitmap data for {Path.GetFileNameWithoutExtension(sourceFile)} at path {sourceBitmapPath}!"); + } + + string destinationBitmapPath = $"{Path.GetDirectoryName(outPath)}{Path.DirectorySeparatorChar}{Path.GetFileName(sourceFile).Split(primaryExtension)[0]}{secondaryExtension}"; + + if (Path.Exists(destinationBitmapPath)) + { + if (verbose) Console.WriteLine($"Found existing bitmap data at {destinationBitmapPath}, overwriting..."); + } + + try + { + if (useGtf && sourceTexture.GetResourcePlatform() == Platform.PS3) + { + PS3TextureUtilities.PS3GTFToDDS(sourcePath, sourceBitmapPath, destinationBitmapPath, verbose); + } + + if (destinationTexture is TextureX360 destX && sourceTexture.GetResourcePlatform() != Platform.X360) + { + destX.Format.MaxMipLevel = destX.Format.MinMipLevel; + } + if (sourceTexture is TextureX360 sourceX) + { + if (sourceX.Format.Tiled && !string.IsNullOrEmpty(sourceBitmapPath)) + { + if (verbose) Console.WriteLine($"Detiling X360 bitmap data for {Path.GetDirectoryName(outPath)}{Path.DirectorySeparatorChar}{Path.GetFileNameWithoutExtension(outPath)}_texture.dat..."); + X360TextureUtilities.WriteUntiled360TextureFile(sourceX, sourceBitmapPath, destinationBitmapPath); + } + } + else + { + if (!TryConvertTexture(sourceTexture, destinationTexture, sourceBitmapPath, destinationBitmapPath)) + { + if (verbose) Console.WriteLine($"Copying associated bitmap data for {Path.GetDirectoryName(outPath)}{Path.DirectorySeparatorChar}{Path.GetFileNameWithoutExtension(outPath)}_texture.dat..."); + File.Copy(sourceBitmapPath, destinationBitmapPath, true); + } + else + { + if (verbose) Console.WriteLine($"Converting associated bitmap data for {Path.GetDirectoryName(outPath)}{Path.DirectorySeparatorChar}{Path.GetFileNameWithoutExtension(outPath)}_texture.dat..."); + } + } + if (verbose) Console.WriteLine($"Wrote texture bitmap data to {destinationFormat} destination directory."); + + if (destinationTexture is TextureBPR destBprTexture && File.Exists(destinationBitmapPath)) + { + destBprTexture.PlacedDataSize = (uint)new FileInfo(destinationBitmapPath).Length; + if (verbose) Console.WriteLine($"BPR PlacedDataSize set to {destBprTexture.PlacedDataSize} (file: {destinationBitmapPath})."); + } + + using FileStream fs = new(outPath, FileMode.Create, FileAccess.Write); + using (EndianAwareBinaryWriter writer = new(fs, destinationTexture.GetResourceEndian())) + { + try + { + if (verbose) Console.WriteLine($"Writing converted {destinationFormat} texture property data to destination file {Path.GetFileName(outPath)}..."); + destinationTexture.WriteToStream(writer); + } + catch + { + throw new IOException("Failed to write converted texture property data to stream."); + } + writer.Close(); + fs.Close(); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error trying to copy bitmap data for {Path.GetFileNameWithoutExtension(sourceFile)}: {ex.Message}"); + } + + Console.WriteLine($"Successfully ported {localSourceFormat} formatted {Path.GetFileNameWithoutExtension(sourceFile)}to {localDestinationFormat} as {Path.GetFileNameWithoutExtension(outPath)}."); + })); + } + + await Task.WhenAll(tasks); + } + + private string BPRx64Hack(TextureBase header, string format) + { + if (header.GetResourcePlatform() == Platform.BPR && format.EndsWith("X64", StringComparison.Ordinal)) + { + header.SetResourceArch(Arch.x64); + return "BPR"; + } + return format; + } + + private static TextureBase? ConstructHeader(string path, string format, bool verbose) + { + if (verbose) Console.WriteLine($"Constructing {format} texture property data..."); + return format switch + { + "BPR" => new TextureBPR(path), + "BPRX64" => new TextureBPR(path), + "TUB" => new TexturePC(path), + "X360" => new TextureX360(path), + "PS3" => new TexturePS3(path), + _ => throw new InvalidPlatformException(), + }; + } + + private static void CopyProperties(TextureBase source, TextureBase destination) + { + if (source == null) throw new ArgumentNullException(nameof(source)); + if (destination == null) throw new ArgumentNullException(nameof(destination)); + + Type srcType = source.GetType(); + Type dstType = destination.GetType(); + + Type typeToReflect = srcType == dstType + ? srcType + : typeof(TextureBase); + + IEnumerable props = typeToReflect + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanRead + && p.CanWrite + && p.GetIndexParameters().Length == 0); + + foreach (PropertyInfo prop in props) + { + object? value = prop.GetValue(source); + prop.SetValue(destination, value); + } + } + + private bool TryConvertTexture(TextureBase srcTexture, TextureBase destTexture, string inPath, string outPath) + { + byte[] bitmap = File.ReadAllBytes(inPath); + switch (srcTexture, destTexture) + { + case (TexturePS3 ps3, TextureBPR bpr): + if (ps3.Format == CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_A8R8G8B8 + && bpr.Format == DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM) + DDSTextureUtilities.A8R8G8B8toB8G8R8A8(bitmap, destTexture.Width, destTexture.Height, destTexture.MipmapLevels); + break; + case (TexturePC tub, TextureBPR bpr): + if (tub.Format == D3DFORMAT.D3DFMT_A8R8G8B8 + && bpr.Format == DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM) + DDSTextureUtilities.A8R8G8B8toB8G8R8A8(bitmap, destTexture.Width, destTexture.Height, destTexture.MipmapLevels); + if (tub.Format == D3DFORMAT.D3DFMT_A8B8G8R8 + && bpr.Format == DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM) + DDSTextureUtilities.A8B8G8R8toB8G8R8A8(bitmap, destTexture.Width, destTexture.Height, destTexture.MipmapLevels); + break; + default: + bitmap = Array.Empty(); + return false; + }; + File.WriteAllBytes(outPath, bitmap); + return true; + } + + private static readonly Dictionary X360toTUBMapping = new() + { + { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT1, D3DFORMAT.D3DFMT_DXT1 }, + { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT2_3, D3DFORMAT.D3DFMT_DXT3 }, + { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT4_5, D3DFORMAT.D3DFMT_DXT5 }, + { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_8, D3DFORMAT.D3DFMT_A8 }, + { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_16_16_16_16, D3DFORMAT.D3DFMT_A16B16G16R16 }, + { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_8_8_8_8, D3DFORMAT.D3DFMT_A8R8G8B8 }, + }; + + private static readonly Dictionary X360toBPRMapping = new() + { + { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT1, DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM }, + { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT2_3, DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM }, + { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT4_5, DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM }, + { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_8, DXGI_FORMAT.DXGI_FORMAT_A8_UNORM }, + { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_16_16_16_16, DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM }, + { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_8_8_8_8, DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM }, + }; + + private static readonly Dictionary TUBtoBPRMapping = new() + { + { D3DFORMAT.D3DFMT_DXT1, DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM }, + { D3DFORMAT.D3DFMT_DXT3, DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM }, + { D3DFORMAT.D3DFMT_DXT5, DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM }, + { D3DFORMAT.D3DFMT_A8, DXGI_FORMAT.DXGI_FORMAT_A8_UNORM }, + { D3DFORMAT.D3DFMT_A8B8G8R8, DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM }, + { D3DFORMAT.D3DFMT_A8R8G8B8, DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM }, + }; + + private static readonly Dictionary BPRtoTUBMapping = new() + { + { DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM, D3DFORMAT.D3DFMT_DXT1 }, + { DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM, D3DFORMAT.D3DFMT_DXT3 }, + { DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM, D3DFORMAT.D3DFMT_DXT5 }, + { DXGI_FORMAT.DXGI_FORMAT_A8_UNORM, D3DFORMAT.D3DFMT_A8 }, + { DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM, D3DFORMAT.D3DFMT_A8B8G8R8 }, + }; + + private static readonly Dictionary PS3toTUBMapping = new() + { + { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT1, D3DFORMAT.D3DFMT_DXT1 }, + { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT23, D3DFORMAT.D3DFMT_DXT3 }, + { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT45, D3DFORMAT.D3DFMT_DXT5 }, + { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_B8, D3DFORMAT.D3DFMT_A8 }, + { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_A8R8G8B8, D3DFORMAT.D3DFMT_A8R8G8B8 }, + }; + + private static readonly Dictionary PS3toX360Mapping = new() + { + { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT1, GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT1 }, + { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT23, GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT2_3 }, + { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT45, GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT4_5 }, + { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_B8, GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_8_B }, + { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_A8R8G8B8, GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_8_8_8_8 }, + }; + + private static readonly Dictionary TUBtoX360Mapping = new() + { + { D3DFORMAT.D3DFMT_DXT1, GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT1 }, + { D3DFORMAT.D3DFMT_DXT3, GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT2_3 }, + { D3DFORMAT.D3DFMT_DXT5, GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT4_5 }, + }; + + private static readonly Dictionary X360toPS3Mapping = new() + { + { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT1, CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT1 }, + { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT2_3, CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT23 }, + { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT4_5, CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT45 }, + { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_8_B, CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_B8 }, + { GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_8_8_8_8, CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_A8R8G8B8 }, + }; + + private static readonly Dictionary PS3toBPRMapping = new() + { + { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT1, DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM }, + { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT23, DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM }, + { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT45, DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM }, + { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_B8, DXGI_FORMAT.DXGI_FORMAT_A8_UNORM }, + { CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_A8R8G8B8, DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM } + }; + + private static readonly Dictionary BPRtoPS3Mapping = new() + { + { DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM, CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT1 }, + { DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM, CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT23 }, + { DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM, CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT45 }, + { DXGI_FORMAT.DXGI_FORMAT_A8_UNORM, CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_B8 }, + }; + + private static readonly Dictionary BPRtoX360Mapping = new() + { + { DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM, GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT1 }, + { DXGI_FORMAT.DXGI_FORMAT_BC2_UNORM, GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT2_3 }, + { DXGI_FORMAT.DXGI_FORMAT_BC3_UNORM, GPUTEXTUREFORMAT.GPUTEXTUREFORMAT_DXT4_5 }, + }; +} diff --git a/Volatility/Operations/Resources/SaveResourceOperation.cs b/Volatility/Operations/Resources/SaveResourceOperation.cs new file mode 100644 index 0000000..8b8e48d --- /dev/null +++ b/Volatility/Operations/Resources/SaveResourceOperation.cs @@ -0,0 +1,35 @@ +using YamlDotNet.Serialization; + +using Volatility.Resources; +using Volatility.Utilities; + +namespace Volatility.Operations.Resources; + +internal class SaveResourceOperation +{ + private readonly ISerializer serializer; + + public SaveResourceOperation() + { + serializer = new SerializerBuilder() + .DisableAliases() + .WithTypeInspector(inner => new IncludeFieldsTypeInspector(inner)) + .WithTypeConverter(new ResourceYamlTypeConverter()) + .WithTypeConverter(new StrongIDYamlTypeConverter()) + .WithTypeConverter(new StringEnumYamlTypeConverter()) + .Build(); + } + + public async Task ExecuteAsync(Resource resource, string filePath) + { + string? directoryPath = Path.GetDirectoryName(filePath); + + if (!string.IsNullOrEmpty(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + + string serializedString = serializer.Serialize(resource); + await File.WriteAllTextAsync(filePath, serializedString); + } +} diff --git a/Volatility/Operations/StringTables/ImportStringTableOperation.cs b/Volatility/Operations/StringTables/ImportStringTableOperation.cs new file mode 100644 index 0000000..b70aeb5 --- /dev/null +++ b/Volatility/Operations/StringTables/ImportStringTableOperation.cs @@ -0,0 +1,86 @@ +using System.Text; +using System.Xml.Linq; + +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +using static Volatility.Utilities.ResourceIDUtilities; +using static Volatility.Utilities.DictUtilities; + +namespace Volatility.Operations.StringTables; + +internal class ImportStringTableOperation +{ + private readonly MergeStringTableEntriesOperation mergeOperation; + + public ImportStringTableOperation(MergeStringTableEntriesOperation mergeOperation) + { + this.mergeOperation = mergeOperation; + } + + public async Task ExecuteAsync(IEnumerable filePaths, Dictionary> entries, string endian, bool overwrite, bool verbose, string yamlFile) + { + var serializer = new SerializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + + var results = await Task.WhenAll(filePaths.Select(path => ProcessFileAsync(path, endian, overwrite, verbose))); + + foreach (var fileResult in results) + { + mergeOperation.Execute(entries, fileResult, overwrite); + } + + string yaml = serializer.Serialize(entries); + await File.WriteAllTextAsync(yamlFile, yaml, Encoding.UTF8); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + } + + private async Task>> ProcessFileAsync(string filePath, string endian, bool overwrite, bool verbose) + { + var entriesByType = new Dictionary>(StringComparer.OrdinalIgnoreCase); + string fileName = Path.GetFileName(filePath)!; + string text = Encoding.UTF8.GetString(await File.ReadAllBytesAsync(filePath)); + + int start = text.IndexOf(""); + int end = text.IndexOf("") + "".Length; + if (start < 0 || end <= start) + { + if (verbose) Console.WriteLine($"Skipping (no table): {fileName}"); + return entriesByType; + } + + XDocument xmlDoc = XDocument.Parse(text[start..end]); + var entries = xmlDoc.Descendants("Resource") + .Select(x => new + { + Id = endian == "be" + ? FlipResourceIDEndian((string)x.Attribute("id")!) + : (string)x.Attribute("id")!, + Type = (string)x.Attribute("type")!, + Name = (string)x.Attribute("name")! + }).ToList(); + + foreach (var e in entries) + { + var dict = entriesByType.GetOrCreate(e.Type, () => new Dictionary()); + if (!dict.TryGetValue(e.Id, out StringTableResourceEntry? existing)) + { + dict[e.Id] = new StringTableResourceEntry { Name = e.Name, Appearances = { fileName } }; + if (verbose) Console.WriteLine($"Found {e.Type} entry in {Path.GetFileName(filePath)} - {e.Name}"); + } + else + { + if (overwrite) + existing.Name = e.Name; + if (!existing.Appearances.Contains(fileName)) + existing.Appearances.Add(fileName); + } + } + + return entriesByType; + } +} diff --git a/Volatility/Operations/StringTables/LoadResourceDictionaryOperation.cs b/Volatility/Operations/StringTables/LoadResourceDictionaryOperation.cs new file mode 100644 index 0000000..2c2e4e0 --- /dev/null +++ b/Volatility/Operations/StringTables/LoadResourceDictionaryOperation.cs @@ -0,0 +1,30 @@ +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace Volatility.Operations.StringTables; + +internal class LoadResourceDictionaryOperation +{ + private readonly IDeserializer deserializer; + + public LoadResourceDictionaryOperation() + { + deserializer = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + } + + public async Task>> ExecuteAsync(string yamlFile) + { + if (!File.Exists(yamlFile)) + { + return new Dictionary>(StringComparer.OrdinalIgnoreCase); + } + + string content = await File.ReadAllTextAsync(yamlFile); + + Dictionary>? result = deserializer.Deserialize>>(content); + + return result ?? new Dictionary>(StringComparer.OrdinalIgnoreCase); + } +} diff --git a/Volatility/Operations/StringTables/MergeStringTableEntriesOperation.cs b/Volatility/Operations/StringTables/MergeStringTableEntriesOperation.cs new file mode 100644 index 0000000..bf9534f --- /dev/null +++ b/Volatility/Operations/StringTables/MergeStringTableEntriesOperation.cs @@ -0,0 +1,38 @@ +namespace Volatility.Operations.StringTables; + +internal class MergeStringTableEntriesOperation +{ + public void Execute(Dictionary> target, Dictionary> source, bool overwrite) + { + foreach ((string typeKey, Dictionary resourceEntries) in source) + { + if (!target.TryGetValue(typeKey, out Dictionary? typeDict)) + { + target[typeKey] = new Dictionary(resourceEntries, StringComparer.OrdinalIgnoreCase); + continue; + } + + foreach ((string resourceKey, StringTableResourceEntry entry) in resourceEntries) + { + if (!typeDict.TryGetValue(resourceKey, out StringTableResourceEntry? existing)) + { + typeDict[resourceKey] = entry; + continue; + } + + if (overwrite) + { + existing.Name = entry.Name; + } + + foreach (string appearance in entry.Appearances) + { + if (!existing.Appearances.Contains(appearance)) + { + existing.Appearances.Add(appearance); + } + } + } + } + } +} diff --git a/Volatility/Operations/StringTables/StringTableResourceEntry.cs b/Volatility/Operations/StringTables/StringTableResourceEntry.cs new file mode 100644 index 0000000..efdc645 --- /dev/null +++ b/Volatility/Operations/StringTables/StringTableResourceEntry.cs @@ -0,0 +1,7 @@ +namespace Volatility.Operations.StringTables; + +internal class StringTableResourceEntry +{ + public string Name { get; set; } = string.Empty; + public List Appearances { get; set; } = new(); +}