diff --git a/BrotliUtils.cs b/BrotliUtils.cs new file mode 100644 index 0000000..6712d7e --- /dev/null +++ b/BrotliUtils.cs @@ -0,0 +1,105 @@ +using System.IO.Compression; + +namespace USSR.Utilities +{ + public class BrotliUtils + { + private static CompressionLevel GetCompressionLevel() + { + if (Enum.IsDefined(typeof(CompressionLevel), 3)) // NOTE: CompressionLevel.SmallestSize == 3 is not supported in .NET Core 3.1 + { + return (CompressionLevel)3; + } + return CompressionLevel.Optimal; + } + + public static byte[] CompressBytes(byte[] bytes) => + CompressBytesAsync(bytes).GetAwaiter().GetResult(); + + public static async Task CompressBytesAsync( + byte[] bytes, + CancellationToken cancel = default + ) + { + using MemoryStream? outputStream = new(); + using (BrotliStream? compressionStream = new(outputStream, GetCompressionLevel())) + { + await compressionStream.WriteAsync(bytes, cancel); + } + return outputStream.ToArray(); + } + + public static void CompressFile(string originalFileName, string compressedFileName) => + CompressFileAsync(originalFileName, compressedFileName).GetAwaiter().GetResult(); + + public static async Task CompressFileAsync( + string originalFileName, + string compressedFileName, + CancellationToken cancel = default + ) + { + using FileStream originalStream = File.Open(originalFileName, FileMode.Open); + using FileStream compressedStream = File.Create(compressedFileName); + await CompressStreamAsync(originalStream, compressedStream, cancel); + } + + public static void CompressStream(Stream originalStream, Stream compressedStream) => + CompressStreamAsync(originalStream, compressedStream).GetAwaiter().GetResult(); + + public static async Task CompressStreamAsync( + Stream originalStream, + Stream compressedStream, + CancellationToken cancel = default + ) + { + using BrotliStream? compressor = new(compressedStream, GetCompressionLevel()); + await originalStream.CopyToAsync(compressor, cancel); + } + + public static byte[] DecompressBytes(byte[] bytes) => + DecompressBytesAsync(bytes).GetAwaiter().GetResult(); + + public static async Task DecompressBytesAsync( + byte[] bytes, + CancellationToken cancel = default + ) + { + using MemoryStream? inputStream = new(bytes); + using MemoryStream outputStream = new(); + using (BrotliStream compressionStream = new(inputStream, CompressionMode.Decompress)) + { + await compressionStream.CopyToAsync(outputStream, cancel); + } + return outputStream.ToArray(); + } + + public static void DecompressFile(string compressedFileName, string outputFileName) => + DecompressFileAsync(compressedFileName, outputFileName).GetAwaiter().GetResult(); + + public static async Task DecompressFileAsync( + string compressedFileName, + string outputFileName, + CancellationToken cancel = default + ) + { + using FileStream compressedFileStream = File.Open(compressedFileName, FileMode.Open); + using FileStream outputFileStream = File.Create(outputFileName); + await DecompressStreamAsync(compressedFileStream, outputFileStream, cancel); + } + + public static void DecompressStream(Stream compressedStream, Stream outputStream) => + DecompressStreamAsync(compressedStream, outputStream).GetAwaiter().GetResult(); + + public static async Task DecompressStreamAsync( + Stream compressedStream, + Stream outputStream, + CancellationToken cancel = default + ) + { + using BrotliStream decompressor = new(compressedStream, CompressionMode.Decompress); + await decompressor.CopyToAsync(outputStream, cancel); + } + } +} + +// Source: https://www.prowaretech.com/articles/current/dot-net/compression-brotli diff --git a/GZipUtils.cs b/GZipUtils.cs new file mode 100644 index 0000000..7a6afb0 --- /dev/null +++ b/GZipUtils.cs @@ -0,0 +1,105 @@ +using System.IO.Compression; + +namespace USSR.Utilities +{ + public class GZipUtils + { + private static CompressionLevel GetCompressionLevel() + { + if (Enum.IsDefined(typeof(CompressionLevel), 3)) // NOTE: CompressionLevel.SmallestSize == 3 is not supported in .NET Core 3.1 but is in .NET 6 + { + return (CompressionLevel)3; + } + return CompressionLevel.Optimal; + } + + public static byte[] CompressBytes(byte[] bytes) => + CompressBytesAsync(bytes).GetAwaiter().GetResult(); + + public static async Task CompressBytesAsync( + byte[] bytes, + CancellationToken cancel = default + ) + { + using MemoryStream? outputStream = new(); + using (GZipStream? compressionStream = new(outputStream, GetCompressionLevel())) + { + await compressionStream.WriteAsync(bytes, cancel); + } + return outputStream.ToArray(); + } + + public static void CompressFile(string originalFileName, string compressedFileName) => + CompressFileAsync(originalFileName, compressedFileName).GetAwaiter().GetResult(); + + public static async Task CompressFileAsync( + string originalFileName, + string compressedFileName, + CancellationToken cancel = default + ) + { + using FileStream originalStream = File.Open(originalFileName, FileMode.Open); + using FileStream compressedStream = File.Create(compressedFileName); + await CompressStreamAsync(originalStream, compressedStream, cancel); + } + + public static void CompressStream(Stream originalStream, Stream compressedStream) => + CompressStreamAsync(originalStream, compressedStream).GetAwaiter().GetResult(); + + public static async Task CompressStreamAsync( + Stream originalStream, + Stream compressedStream, + CancellationToken cancel = default + ) + { + using GZipStream? compressor = new(compressedStream, GetCompressionLevel()); + await originalStream.CopyToAsync(compressor, cancel); + } + + public static byte[] DecompressBytes(byte[] bytes) => + DecompressBytesAsync(bytes).GetAwaiter().GetResult(); + + public static async Task DecompressBytesAsync( + byte[] bytes, + CancellationToken cancel = default + ) + { + using MemoryStream? inputStream = new(bytes); + using MemoryStream? outputStream = new(); + using (GZipStream? compressionStream = new(inputStream, CompressionMode.Decompress)) + { + await compressionStream.CopyToAsync(outputStream, cancel); + } + return outputStream.ToArray(); + } + + public static void DecompressFile(string compressedFileName, string outputFileName) => + DecompressFileAsync(compressedFileName, outputFileName).GetAwaiter().GetResult(); + + public static async Task DecompressFileAsync( + string compressedFileName, + string outputFileName, + CancellationToken cancel = default + ) + { + using FileStream compressedFileStream = File.Open(compressedFileName, FileMode.Open); + using FileStream outputFileStream = File.Create(outputFileName); + await DecompressStreamAsync(compressedFileStream, outputFileStream, cancel); + } + + public static void DecompressStream(Stream compressedStream, Stream outputStream) => + DecompressStreamAsync(compressedStream, outputStream).GetAwaiter().GetResult(); + + public static async Task DecompressStreamAsync( + Stream compressedStream, + Stream outputStream, + CancellationToken cancel = default + ) + { + using GZipStream? decompressor = new(compressedStream, CompressionMode.Decompress); + await decompressor.CopyToAsync(outputStream, cancel); + } + } +} + +// Source: https://www.prowaretech.com/articles/current/dot-net/compression-gzip diff --git a/Program.cs b/Program.cs index b48bc76..6a3c6d4 100644 --- a/Program.cs +++ b/Program.cs @@ -1,150 +1,199 @@ using System.Reflection; using AssetsTools.NET; using AssetsTools.NET.Extra; +using USSR.Utilities; -namespace Kiraio.USSR +namespace USSR { public class Program { - const string VERSION = "1.0.0"; + const string VERSION = "1.1.0"; const string ASSET_CLASS_DB = "classdata.tpk"; public enum LoadTypes { Asset, - Bundle + Bundle, + WebData } - static LoadTypes loadTypes; + static LoadTypes loadType; static void Main(string[] args) { if (args.Length < 1) { Console.WriteLine($"Unity Splash Screen Remover (USSR) v{VERSION} \n"); - Console.WriteLine("USSR is a CLI tool to remove Unity splash screen logo."); Console.WriteLine( - "USSR didn't directly \"hack\" Unity Editor, but the generated build. So, not all platforms is supported!" + "USSR is a CLI tool to remove Unity splash screen logo while keep your logo displayed." + ); + Console.WriteLine( + "USSR didn't directly \"hack\" Unity Editor, instead the generated build. So, not all platforms is supported." ); Console.WriteLine( "For more information, visit USSR repo: https://github.com/kiraio-moe/USSR \n" ); - Console.WriteLine("Usage: USSR.exe "); + Console.WriteLine("Usage: USSR.exe "); Console.WriteLine( - "Alternatively, you can drag and drop game executable to USSR.exe" + "Alternatively, you can just drag and drop your game executable to USSR.exe" ); Console.ReadLine(); return; } - string? ussrPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - string? assetClassTypesPackage = Path.Combine(ussrPath, ASSET_CLASS_DB); + string? ussrExec = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + string? tpkFile = Path.Combine(ussrExec, ASSET_CLASS_DB); - if (!File.Exists(assetClassTypesPackage)) + AssetsManager assetsManager = new(); + + try { - Console.WriteLine($"Asset class types package not found: {assetClassTypesPackage}"); - Console.WriteLine("The file is moved or deleted."); + Console.WriteLine("Loading class type package..."); + assetsManager.LoadClassPackage(tpkFile); + } + catch (Exception ex) + { + Console.WriteLine($"Asset class types package not found. {ex.Message}"); Console.ReadLine(); return; } - Console.WriteLine("Loading class type package..."); - AssetsManager? assetsManager = new(); - assetsManager.LoadClassPackage(assetClassTypesPackage); - string? execPath = args[0]; string? execExtension = Path.GetExtension(execPath); - string? rootPath = Path.GetDirectoryName(execPath); - string? targetFile; + string? execDirectory = Path.GetDirectoryName(execPath); + + string? dataFile; + string? dataDirectory; // game data directory + + string? webDataFile = null; // WebGL.[data, data.br, data.gz] + string[] webDataFileExtensions = { ".data", ".data.br", ".data.gz" }; + string? compressionType = null; + FileStream? bundleStream = null; + + // List of files to be deleted List temporaryFiles = new(); - Console.WriteLine("Checking for supported platforms..."); switch (execExtension) { case ".exe": case ".x86": case ".x86_64": case ".dmg": - Console.WriteLine("Supported."); - - string? dataPath = Path.Combine( - rootPath, + dataDirectory = Path.Combine( + execDirectory, $"{Path.GetFileNameWithoutExtension(execPath)}_Data" ); + break; + case ".html": + // Set to the Build folder first + dataDirectory = Path.Combine(execDirectory, "Build"); - Console.WriteLine("Checking for globalgamemanagers..."); + // Search for WebGL.* file + foreach (string extension in webDataFileExtensions) + { + webDataFile = Path.Combine(dataDirectory, $"WebGL{extension}"); - // Default compression - targetFile = Path.Combine(dataPath, "globalgamemanagers"); - loadTypes = LoadTypes.Asset; + if (File.Exists(webDataFile)) + { + // Set compression type + compressionType = extension; + break; + } + } - // LZMA/LZ4 compression - if (!File.Exists(targetFile)) + if (!Utility.CheckFile(webDataFile)) { - Console.WriteLine( - "globalgamemanagers not found. Checking for data.unity3d instead..." - ); + Console.ReadLine(); + return; + } + + Utility.BackupOnlyOnce(webDataFile); + + string? decompressedWebData = Path.Combine(dataDirectory, "WebGL.data"); + temporaryFiles.Add(decompressedWebData); + loadType = LoadTypes.WebData; - targetFile = Path.Combine(dataPath, "data.unity3d"); - loadTypes = LoadTypes.Bundle; + switch (compressionType) + { + case ".data.br": + BrotliUtils.DecompressFile(webDataFile, decompressedWebData); + break; + case ".data.gz": + GZipUtils.DecompressFile(webDataFile, decompressedWebData); + break; } + // Unpack WebGL.data and set the dataDirectory to output folder + dataDirectory = UnityWebDataHelper.UnpackBundleToFile(decompressedWebData); + break; - case ".html": - // TODO: Process WebGL.data - // targetFile = Path.Combine(rootPath, "Build", "WebGL.data"); - // break; - return; default: Console.WriteLine("Sorry, unsupported platform :("); Console.ReadLine(); return; } - // Check game data - if (!File.Exists(targetFile)) + Console.WriteLine("Checking for globalgamemanagers..."); + + // Default compression + dataFile = Path.Combine(dataDirectory, "globalgamemanagers"); + loadType = LoadTypes.Asset; + + // LZ4/LZ4HC compression + if (!File.Exists(dataFile)) + { + Console.WriteLine( + "globalgamemanagers not found. Checking for data.unity3d instead..." + ); + + dataFile = Path.Combine(dataDirectory, "data.unity3d"); + loadType = LoadTypes.Bundle; + } + + if (!Utility.CheckFile(dataFile)) { - Console.WriteLine($"{targetFile} doesn't exists!"); - Console.WriteLine("The file is moved or deleted."); Console.ReadLine(); return; } - // Make temporary copy - string? inspectedFile = Utility.CloneFile(targetFile, $"{targetFile}.temp"); - temporaryFiles.Add(inspectedFile); + // Only backup globalgamemanagers or data.unity3d if not in WebGL + if (loadType.Equals(LoadTypes.WebData)) + Utility.BackupOnlyOnce(dataFile); + + // Make temporary copy, so the original file ready to be overwritten + string? tempFile = Utility.CloneFile(dataFile, $"{dataFile}.temp"); + temporaryFiles.Add(tempFile); AssetsFileInstance? assetFileInstance = null; BundleFileInstance? bundleFileInstance = null; - FileStream? bundleStream = null; try { - switch (loadTypes) + switch (loadType) { // globalgamemanagers case LoadTypes.Asset: // Load target file and it's dependencies // Loading the dependencies is required to check unity logo asset Console.WriteLine("Loading asset file and it's dependencies..."); - assetFileInstance = assetsManager.LoadAssetsFile(inspectedFile, true); + assetFileInstance = assetsManager.LoadAssetsFile(tempFile, true); + break; // data.unity3d case LoadTypes.Bundle: - Console.WriteLine("Loading asset bundle file..."); - bundleFileInstance = assetsManager.LoadBundleFile(inspectedFile, false); + Console.WriteLine("Unpacking asset bundle file..."); - string? bundleStreamFile = $"{targetFile}.stream"; - bundleStream = File.Open(bundleStreamFile, FileMode.Create); + string? unpackedBundleFile = $"{dataFile}.unpacked"; + temporaryFiles.Add(unpackedBundleFile); + bundleStream = File.Open(unpackedBundleFile, FileMode.Create); + + bundleFileInstance = assetsManager.LoadBundleFile(tempFile, false); bundleFileInstance.file = BundleHelper.UnpackBundleToStream( bundleFileInstance.file, bundleStream ); - // Add to cleanup chores - temporaryFiles.Add(bundleStreamFile); - Console.WriteLine("Loading asset file and it's dependencies..."); assetFileInstance = assetsManager.LoadAssetsFileFromBundle( bundleFileInstance, @@ -200,7 +249,7 @@ static void Main(string[] args) "Unity splash screen logo didn't exist or already removed. Nothing to do." ); - bundleStream?.Close(); + // bundleStream?.Close(); assetsManager.UnloadAll(true); Utility.CleanUp(temporaryFiles); @@ -208,14 +257,6 @@ static void Main(string[] args) return; } - // Backup original file - string? backupOriginalFile = $"{targetFile}.bak"; - if (!File.Exists(backupOriginalFile)) - { - Console.WriteLine("Backup original file..."); - Utility.CloneFile(targetFile, backupOriginalFile); - } - Console.WriteLine("Removing Unity splash screen..."); // Remove Unity splash screen by flipping these boolean fields @@ -235,11 +276,7 @@ static void Main(string[] args) logoPointer ); - // IDK why AssetsTools won't load "UnitySplash-cube" - // external asset while in Bundle file. So, we can - // check it's name then remove it. - // So, we break it into 2 types of file load. - switch (loadTypes) + switch (loadType) { case LoadTypes.Asset: // Get the base field @@ -252,18 +289,24 @@ static void Main(string[] args) break; case LoadTypes.Bundle: - // After some testing, I realize only Unity - // splash screen logo external asset that - // won't load. So, we can use it to mark - // that this is the Unity splash screen logo + /* + * IDK why AssetsTools won't load "UnitySplash-cube" + * external asset while in Bundle file. So, we can + * check it's name and remove it like before. + * + * Alternatively, we can still find it by checking + * the base field. If it's null, then it is. + */ if (logoExtInfo.baseField == null) unityLogo = data; break; } } - // Remove Unity splash screen logo to completely remove - // Unity splash screen logo. Only our logo remained. + /* + * Remove "UnitySplash-cube" to completely remove + * Unity splash screen logo. So, Only our logo remained. + */ if (unityLogo != null) splashScreenLogos?.Children.Remove(unityLogo); @@ -285,57 +328,55 @@ static void Main(string[] args) ) }; - List bundleReplacers = - new() - { - new BundleReplacerFromAssets( - assetFileInstance?.name, - null, - assetFile, - assetsReplacers - ) - }; - - FileStream? uncompressedBundleStream = null; - try { // Write modified asset file to disk Console.WriteLine("Writing changes to disk..."); - switch (loadTypes) + switch (loadType) { case LoadTypes.Asset: - using (AssetsFileWriter writer = new(targetFile)) - { + using (AssetsFileWriter writer = new(dataFile)) assetFile?.Write(writer, 0, assetsReplacers); - } break; case LoadTypes.Bundle: - string uncompressedBundleFile = $"{targetFile}.uncompressed"; + List bundleReplacers = + new() + { + new BundleReplacerFromAssets( + assetFileInstance?.name, + null, + assetFile, + assetsReplacers + ) + }; + + string uncompressedBundleFile = $"{dataFile}.uncompressed"; temporaryFiles.Add(uncompressedBundleFile); using (AssetsFileWriter writer = new(uncompressedBundleFile)) - { bundleFile?.Write(writer, bundleReplacers); - } - - uncompressedBundleStream = File.OpenRead(uncompressedBundleFile); - - AssetBundleFile? uncompressedBundle = new(); - uncompressedBundle.Read(new AssetsFileReader(uncompressedBundleStream)); - using (AssetsFileReader reader = new(uncompressedBundleStream)) + using ( + FileStream? uncompressedBundleStream = File.OpenRead( + uncompressedBundleFile + ) + ) { Console.WriteLine("Compressing asset bundle file..."); - using AssetsFileWriter writer = new(targetFile); + AssetBundleFile? uncompressedBundle = new(); + uncompressedBundle.Read(new AssetsFileReader(uncompressedBundleStream)); + + // using AssetsFileReader reader = new(uncompressedBundleStream); + using AssetsFileWriter writer = new(dataFile); uncompressedBundle.Pack( uncompressedBundle.Reader, writer, AssetBundleCompressionType.LZ4 ); } + break; } } @@ -350,9 +391,7 @@ static void Main(string[] args) // Cleanup temporary files Console.WriteLine("Cleaning up temporary files..."); - bundleStream?.Close(); - uncompressedBundleStream?.Close(); assetsManager.UnloadAllBundleFiles(); assetsManager.UnloadAllAssetsFiles(true); Utility.CleanUp(temporaryFiles); diff --git a/README.md b/README.md index 7784d07..083fb06 100644 --- a/README.md +++ b/README.md @@ -15,17 +15,19 @@ ## Overview -The Unity Splash Screen Remover is a Command-Line Interface (CLI) tool designed to remove the Unity splash screen logo from Unity-built games. The tool is an implementation of the guide available at [https://github.com/kiraio-moe/remove-unity-splash-screen](https://github.com/kiraio-moe/remove-unity-splash-screen). By utilizing this tool, you can easily remove the Unity splash screen from your games. +The Unity Splash Screen Remover is a Command-Line Interface (CLI) tool designed to remove the Unity splash screen logo from Unity-built games. + +The tool is an implementation of the guide available at [https://github.com/kiraio-moe/remove-unity-splash-screen](https://github.com/kiraio-moe/remove-unity-splash-screen). By utilizing this tool, you can easily remove Unity splash screen logo from your games and keep your own logo displayed. ## Requirements -- [.NET 6.0 SDK](https://dotnet.microsoft.com/download/dotnet/6.0 ".NET 6.0 SDK") +- [.NET 6.0 SDK](https://dotnet.microsoft.com/download/dotnet/6.0 ".NET 6.0 SDK") or [.NET 7.0 SDK](https://dotnet.microsoft.com/download/dotnet/7.0 ".NET 7.0 SDK") ## Usage To use the tool, follow the steps below: -1. Download the Unity Splash Screen Remover from [Releases](https://github.com/kiraio-moe/USSR/releases). +1. Download the Unity Splash Screen Remover from [Releases](https://github.com/kiraio-moe/USSR/releases) page. 2. Drag and drop your game executable to `USSR.exe` or execute USSR as follow: ```bash @@ -36,19 +38,22 @@ To use the tool, follow the steps below: Unity Splash Screen Remover currently supports the following platforms: -- PC, Mac, Linux Standalone (Default Compression) +- PC, Mac, Linux Standalone (Support Default, LZ4 & LZ4HC Compression) ## Known Supported Unity Versions -- 2020 -- 2019 +- 2019 ~ 2022 For other versions, please test it yourself and let me know! ## Todo List -- Support WebGL platform. -- Support compressed build. +- Add support for WebGL platform + - Decompress WebGL.data ✔️ + - Compress back to WebGL.data (WIP) + - Support Brotli compression ✔️ + - Support GZip compression ✔️ +- ~~Support compressed build~~ ✔️ ## Credits diff --git a/USSR.csproj b/USSR.csproj index 13ffb49..28af4cd 100644 --- a/USSR.csproj +++ b/USSR.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net6.0;net7.0 enable enable @@ -10,6 +10,7 @@ + diff --git a/UnityWebDataHelper.cs b/UnityWebDataHelper.cs new file mode 100644 index 0000000..fbea634 --- /dev/null +++ b/UnityWebDataHelper.cs @@ -0,0 +1,54 @@ +using Kaitai; + +namespace USSR.Utilities +{ + public class UnityWebDataHelper + { + /// + /// Unpack UnityWebData (WebGL.data) to File. + /// + /// + /// Output directory. + public static string UnpackBundleToFile(string? bundleFile) + { + if (!File.Exists(bundleFile)) + throw new FileNotFoundException($"{bundleFile} didn\'t exist!"); + + // Create the Kaitai stream and the root object from the parsed data + UnityWebData? unityWebData = UnityWebData.FromFile(bundleFile); + + string? outputDirectory = Path.Combine( + Path.GetDirectoryName(bundleFile), + Path.GetFileNameWithoutExtension(bundleFile) + ); + if (!Directory.Exists(outputDirectory)) + Directory.CreateDirectory(outputDirectory); + + Console.WriteLine("Extracting bundle file..."); + + foreach (UnityWebData.FileEntry fileEntry in unityWebData.Files) + { + string? fileName = fileEntry?.Filename; + + // Create file entry directory + string? fileNameDirectory = Path.Combine( + outputDirectory, + Path.GetDirectoryName(fileName) + ); + if (!Directory.Exists(fileNameDirectory)) + Directory.CreateDirectory(fileNameDirectory); + + string? outputFile = Path.Combine(outputDirectory, fileName); + + using FileStream? outputFileStream = new(outputFile, FileMode.Create); + outputFileStream?.Write(fileEntry?.Data); + } + + Console.WriteLine("Extraction complete."); + return outputDirectory; + } + + // TODO: Pack files to WebGL.data + public static void PackFilesToBundle(string? sourceDirectory) { } + } +} diff --git a/Utility.cs b/Utility.cs index c6b05c3..e25f715 100644 --- a/Utility.cs +++ b/Utility.cs @@ -1,4 +1,4 @@ -namespace Kiraio.USSR +namespace USSR.Utilities { public class Utility { @@ -15,9 +15,7 @@ public static string CloneFile(string sourceFilePath, string backupDestinationPa // Check if the source file exists if (!File.Exists(sourceFilePath)) { - return new FileNotFoundException( - "Backup source file does not exist!" - ).ToString(); + throw new FileNotFoundException("Backup source file doesn\'t exist!"); } // Create the backup destination directory if it doesn't exist @@ -39,21 +37,81 @@ public static string CloneFile(string sourceFilePath, string backupDestinationPa } /// - /// Delete unnecessary . + /// Backup a file. If it's already exist, skip. + /// + /// + /// + public static string BackupOnlyOnce(string? sourceFile) + { + string backupFile = $"{sourceFile}.bak"; + + if (!File.Exists(backupFile)) + { + Console.WriteLine("Backup original file..."); + + CloneFile(sourceFile, backupFile); + } + + return backupFile; + } + + /// + /// Delete . /// /// public static void CleanUp(List? files) { - if (files.Count < 1) + if (files?.Count < 1) return; foreach (string file in files) { if (File.Exists(file)) - { File.Delete(file); - } } } + + /// + /// Check if File exists. + /// + /// + /// + public static bool CheckFile(string? file) + { + if (!File.Exists(file)) + { + Console.WriteLine($"{file} didn\'t exist! The file is moved or deleted."); + + return false; + } + + return true; + } + + /// + /// Find an asset file. + /// + /// + /// + /// If one is found, return the path. Otherwise null. + public static string? FindAsset( + string[] assetFileList, + string rootDirectory, + string? filePrefix = null, + string? filePostfix = null + ) + { + string? assetFile = null; + + foreach (string file in assetFileList) + { + assetFile = Path.Combine(rootDirectory, "${filePrefix}{file}{filePostfix}"); + + if (File.Exists(assetFile)) + break; + } + + return assetFile; + } } }