diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 76a9892..5485e31 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,8 +15,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - dotnet-version: [ '6.0.x', '7.0.x' ] - dotnet-framework: [ 'net6.0', 'net7.0' ] + dotnet-version: [ '6.0.x', '7.0.x', '8.0.x' ] + dotnet-framework: [ 'net6.0', 'net7.0', 'net8.0' ] dotnet-runtime: [ 'win-x64', 'win-arm64', 'linux-x64', 'linux-arm64', 'osx-x64', 'osx-arm64' ] steps: diff --git a/README.md b/README.md index cef6691..c235aa2 100644 --- a/README.md +++ b/README.md @@ -31,19 +31,15 @@ On the latest updated policy for Unity Runtime Fee on 22 September 2023 (), one of the key updates is Unity has decided to make the splash screen optional. Starting with Unity 2024 LTS (currently referred to as the 2023 LTS) or later. -> ...and we will remove the requirement to use the Made with Unity splash screen (starting with the LTS version releasing in 2024, currently referred to as the 2023 LTS, or later). - -If you developing a new game, you may want to switch to those Unity version. - ## Overview The Unity Splash Screen Remover (USSR) is a Command-Line Interface (CLI) tool designed to remove the Unity splash screen logo & [watermark](https://forum.unity.com/threads/i-am-using-personal-but-there-is-trial-version-water-mark-after-build.591610/#post-3975343) from Unity-built games. -The tool is an implementation of the guide available at . By utilizing this tool, you can easily remove Unity splash screen logo from your games and keep your own logo displayed. +The tool is an implementation of the guide available at . By utilizing this tool, you can easily remove Unity splash screen 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") or [.NET 7.0 SDK](https://dotnet.microsoft.com/download/dotnet/7.0 ".NET 7.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") or [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0 ".NET 8.0 SDK") - Splash screen `Draw Mode` have been set to `All Sequential` in `Player Settings`. ## Usages @@ -52,7 +48,7 @@ The tool is an implementation of the guide available at You can find the nightly build of USSR with the latest bug fixes in the [latest runned workflow](https://github.com/kiraio-moe/USSR/actions/workflows/build.yml) artifacts. - Download USSR from [Releases](https://github.com/kiraio-moe/USSR/releases) page. -- Run `USSR.exe` and follow the instructions. +- Run `USSR.exe` and follow the instructions. If you're on Android or iOS platform, please read the guide below. ### Android @@ -71,7 +67,7 @@ The tool is an implementation of the guide available at [!WARNING] -> This section may have some incorrect information. Feel free to create Issue/Pull Request if there's mistake. +> This section may have some incorrect information (I'm not an expert on this). Feel free to create Issue/Pull Request if there's mistake. - [Xcode](https://developer.apple.com/xcode/ "Xcode") installed. - There's no fancy settings to be checked, just **Build** (export) the project. @@ -92,18 +88,19 @@ USSR support the following platforms: ## Supported Unity Versions > [!IMPORTANT] -> Some minor Unity versions is not supported and can caused crash. +> Some minor Unity versions are not supported and can caused crash. - Unity 5 ~ Unity 2023 ## Contribute -If you found any bugs or have suggestions, feel free to make an Issue/Pull Request. +If you found any bugs or have suggestions, feel free to create an Issue/Pull Request. ## Credits - Special thanks to [@nesrak1](https://github.com/nesrak1) for the [@nesrak1/AssetsTools.NET](https://github.com/nesrak1/AssetsTools.NET) library, which was instrumental in the development of this tool. - [@arti4ikmin](https://github.com/arti4ikmin) for the guide on Android platform. +- [UnityWebTools.NET](https://github.com/kiraio-moe/UnityWebTools.NET), my own tool to unpack & repack UnityWebData. ## License diff --git a/USSR.cs b/USSR.cs index af3fb53..09767d1 100644 --- a/USSR.cs +++ b/USSR.cs @@ -1,6 +1,7 @@ -using System.Reflection; +using System; using AssetsTools.NET; using AssetsTools.NET.Extra; +using Kiraio.UnityWebTools; using NativeFileDialogSharp; using Spectre.Console; using USSR.Utilities; @@ -11,112 +12,6 @@ public class USSR { const string VERSION = "1.1.6"; const string ASSET_CLASS_DB = "classdata.tpk"; - static readonly byte[] ggmMagic = - { - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x16, - 0x00, - 0x00, - 0x00, - 0x00 - }; - - static readonly byte[] unity3dMagic = - { - 0x55, - 0x6E, - 0x69, - 0x74, - 0x79, - 0x46, - 0x53, - 0x00, - 0x00, - 0x00, - 0x00, - 0x08, - 0x35, - 0x2E, - 0x78, - 0x2E - }; - - static readonly byte[] unityWebDataMagic = - { - 0x55, - 0x6E, - 0x69, - 0x74, - 0x79, - 0x57, - 0x65, - 0x62, - 0x44, - 0x61, - 0x74, - 0x61, - 0x31, - 0x2E, - 0x30, - 0x00 - }; - - static readonly byte[] gzipMagic = { 0x1f, 0x8b }; - - // Luckily, Unity have customized Brotli specification, so we can detect it. - // Ref: https://github.com/google/brotli/issues/867#issue-739852869 - static readonly byte[] unityBrotliMagic = - { - 0x6B, - 0x8D, - 0x00, - 0x55, - 0x6E, - 0x69, - 0x74, - 0x79, - 0x57, - 0x65, - 0x62, - 0x20, - 0x43, - 0x6F, - 0x6D, - 0x70, - 0x72, - 0x65, - 0x73, - 0x73, - 0x65, - 0x64, - 0x20, - 0x43, - 0x6F, - 0x6E, - 0x74, - 0x65, - 0x6E, - 0x74, - 0x20, - 0x28, - 0x62, - 0x72, - 0x6F, - 0x74, - 0x6C, - 0x69, - 0x29 - }; enum AssetTypes { @@ -124,15 +19,15 @@ enum AssetTypes Bundle } - static AssetTypes assetType; - - enum WebGLCompressionTypes + enum WebGLCompressionType { + None, Brotli, GZip } - static WebGLCompressionTypes webGLCompressionType; + static AssetTypes assetType; + static WebGLCompressionType webGLCompressionType; static void Main(string[] args) { @@ -140,20 +35,19 @@ static void Main(string[] args) PrintHelp(); Console.WriteLine(); - string? ussrExec = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + string? ussrExec = Path.GetDirectoryName(AppContext.BaseDirectory); ChooseAction: string[] actionList = { "Remove Unity Splash Screen", "Remove Watermark", "Exit" }; string actionPrompt = AnsiConsole.Prompt( new SelectionPrompt() - .Title("What would you like to do?") + .Title("What would you like to do? (Press ENTER go, UP/DOWN to select)") .AddChoices(actionList) ); int choiceIndex = Array.FindIndex(actionList, item => item == actionPrompt); string? selectedFile = string.Empty, - originalFileName = string.Empty; - string? webDataFile = string.Empty, + webDataFile = string.Empty, unpackedWebDataDirectory = string.Empty; bool isWebGL = false; @@ -161,24 +55,33 @@ static void Main(string[] args) { case 0: case 1: - // Unfortunately, only one filter are supported. - // Instead of working around with this, we just need to manually validate by reading the file header later. + AnsiConsole.MarkupLine("Opening File Picker..."); + // Unfortunately, this File Picker library currently only support one filter :( + // So we pass all file types and manually checking them if it's a valid file that we want. DialogResult filePicker = Dialog.FileOpen( null, Path.GetDirectoryName(Utility.GetLastOpenedFile()) ); if (filePicker.IsCancelled) + { + AnsiConsole.MarkupLine("Cancelled. Oh, it\'s okay ^_^"); + Console.WriteLine(); goto ChooseAction; + } else if (filePicker.IsError) { AnsiConsole.MarkupLine( - "[red]( ERROR )[/]Unable to open File Picker dialog!" + "[red]( RAWR )[/] Unable to open File Picker! Try using a different Terminal?" ); + Console.WriteLine(); goto ChooseAction; } - selectedFile = originalFileName = filePicker.Path; + selectedFile = filePicker.Path; + AnsiConsole.MarkupLineInterpolated( + $"( INFO ) Selected file: [green]{selectedFile}[/]" + ); Utility.SaveLastOpenedFile(selectedFile); break; case 2: @@ -189,50 +92,75 @@ static void Main(string[] args) webDataFile = Path.Combine( Path.GetDirectoryName(selectedFile) ?? string.Empty, - Path.GetFileNameWithoutExtension(selectedFile) + Path.GetFileNameWithoutExtension(selectedFile) // Without .br / .gz extension ); + string selectedFileName = Path.GetFileName(selectedFile); - if (Utility.ValidateFile(selectedFile, ggmMagic)) - { - AnsiConsole.MarkupLine("( INFO ) [green]globalgamemanagers[/] file selected."); + if (selectedFileName.Contains("globalgamemanagers")) assetType = AssetTypes.Asset; - } - else if (Utility.ValidateFile(selectedFile, unity3dMagic)) - { - AnsiConsole.MarkupLine("( INFO ) [green]unity3d[/] file selected."); + else if (selectedFileName.EndsWith(".unity3d")) assetType = AssetTypes.Bundle; - } - else if (Utility.ValidateFile(selectedFile, unityWebDataMagic)) + else if (selectedFileName.EndsWith(".data")) { - AnsiConsole.MarkupLine("( INFO ) [green]UnityWebData[/] file selected."); isWebGL = true; webDataFile = selectedFile; + webGLCompressionType = WebGLCompressionType.None; } - else if (Utility.ValidateFile(selectedFile, unityBrotliMagic) || Path.GetExtension(selectedFile) == ".br") + else if (selectedFileName.EndsWith("data.unityweb")) { - AnsiConsole.MarkupLine("( INFO ) [green]UnityWebData Brotli[/] file selected."); + string[] compressionList = { "Brotli", "GZip" }; + string compressionListPrompt = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("What compression type did you use?") + .AddChoices(compressionList) + ); + int compressionChoiceIndex = Array.FindIndex( + compressionList, + item => item == compressionListPrompt + ); isWebGL = true; - webGLCompressionType = WebGLCompressionTypes.Brotli; - AnsiConsole.MarkupLineInterpolated( - $"( INFO ) Decompressing [green]{selectedFile}[/]..." - ); - BrotliUtils.DecompressFile(selectedFile, webDataFile); + switch (compressionChoiceIndex) + { + case 0: + webGLCompressionType = WebGLCompressionType.Brotli; + if ( + DecompressCompressedWebData( + webGLCompressionType, + selectedFile, + webDataFile + ) == 1 + ) + goto ChooseAction; + break; + case 1: + webGLCompressionType = WebGLCompressionType.GZip; + if ( + DecompressCompressedWebData( + webGLCompressionType, + selectedFile, + webDataFile + ) == 1 + ) + goto ChooseAction; + break; + } } - else if (Utility.ValidateFile(selectedFile, gzipMagic)) + else if (selectedFileName.EndsWith("data.br")) { - AnsiConsole.MarkupLine("( INFO ) [green]UnityWebData GZip[/] file selected."); isWebGL = true; - webGLCompressionType = WebGLCompressionTypes.GZip; - - AnsiConsole.MarkupLineInterpolated( - $"( INFO ) Decompressing [green]{selectedFile}[/]..." - ); - GZipUtils.DecompressFile(selectedFile, webDataFile); + webGLCompressionType = WebGLCompressionType.Brotli; + DecompressCompressedWebData(webGLCompressionType, selectedFile, webDataFile); + } + else if (selectedFileName.EndsWith("data.gz")) + { + isWebGL = true; + webGLCompressionType = WebGLCompressionType.GZip; + DecompressCompressedWebData(webGLCompressionType, selectedFile, webDataFile); } else { - AnsiConsole.MarkupLine("[red]( ERROR )[/] Unknown/Unsupported file type!"); + AnsiConsole.MarkupLine("[red]( RAWR )[/] Unknown/Unsupported file type!"); Console.WriteLine(); goto ChooseAction; } @@ -248,7 +176,7 @@ static void Main(string[] args) if (isWebGL) { // Unpack WebData asset + add to temporary files - unpackedWebDataDirectory = UnityWebDataHelper.UnpackWebDataToFile(webDataFile); + unpackedWebDataDirectory = UnityWebTool.Unpack(webDataFile); // Find and select "data.unity3d" or "globalgamemanagers" inspectedFile = Utility.FindRequiredAsset(unpackedWebDataDirectory); @@ -256,13 +184,14 @@ static void Main(string[] args) // Determine the asset type if (inspectedFile.Contains("globalgamemanagers")) assetType = AssetTypes.Asset; - else if (inspectedFile.Contains("unity3d")) + else if (inspectedFile.EndsWith(".unity3d")) assetType = AssetTypes.Bundle; } AssetsFileInstance? assetFileInstance = null; BundleFileInstance? bundleFileInstance = null; FileStream? bundleStream = null; + List? assetsReplacer = null; string tempFile = Utility.CloneFile(inspectedFile, $"{inspectedFile}.temp"); temporaryFiles.Add(tempFile); @@ -287,20 +216,27 @@ static void Main(string[] args) break; } - AnsiConsole.MarkupLine("( INFO ) Loading asset class types database..."); - if (assetFileInstance != null) + try + { + AnsiConsole.MarkupLine("( INFO ) Loading asset class types database..."); assetsManager.LoadClassDatabaseFromPackage( assetFileInstance?.file.Metadata.UnityVersion ); - else - AnsiConsole.MarkupLine( - "[red]( ERROR )[/] Unable to load asset class types database!" + } + catch (Exception ex) + { + AnsiConsole.MarkupLineInterpolated( + $"[red]( RAWR )[/] Error when loading asset class types database! {ex.Message}" ); - - List? assetsReplacer = null; + goto Cleanup; + } if (assetFileInstance != null) { + AnsiConsole.MarkupLineInterpolated( + $"( INFO ) [bold]Unity Version[/]: [green]{assetFileInstance?.file.Metadata.UnityVersion.ToString()}[/]" + ); + switch (choiceIndex) { case 0: @@ -324,57 +260,67 @@ static void Main(string[] args) } } + Cleanup: bundleStream?.Close(); assetsManager?.UnloadAll(true); Utility.CleanUp(temporaryFiles); // After writing the changes and cleaning the temporary files, - // it's time to pack the extracted WebData back - if (isWebGL) + // it's time to pack the extracted WebData. + try { - // Only pack if the contents is modified - if (assetsReplacer != null) + if (isWebGL && assetsReplacer != null) { - if ( - webGLCompressionType == WebGLCompressionTypes.Brotli - || webGLCompressionType == WebGLCompressionTypes.GZip - ) - UnityWebDataHelper.PackFilesToWebData( - unpackedWebDataDirectory, - webDataFile - ); - + AnsiConsole.MarkupLineInterpolated( + $"( INFO ) Packing [green]{unpackedWebDataDirectory}[/]..." + ); switch (webGLCompressionType) { - case WebGLCompressionTypes.Brotli: + case WebGLCompressionType.Brotli: + UnityWebTool.Pack(unpackedWebDataDirectory, webDataFile); + AnsiConsole.MarkupLineInterpolated( $"( INFO ) Compressing [green]{webDataFile}[/] using Brotli compression. Please be patient, it might take some time..." ); BrotliUtils.CompressFile(webDataFile, selectedFile); - // BrotliUtils.WriteUnityIdentifier(selectedFile, unityBrotliMagic); break; - case WebGLCompressionTypes.GZip: + case WebGLCompressionType.GZip: + UnityWebTool.Pack(unpackedWebDataDirectory, webDataFile); + AnsiConsole.MarkupLineInterpolated( $"( INFO ) Compressing [green]{webDataFile}[/] using GZip compression. Please be patient, it might take some time..." ); GZipUtils.CompressFile(webDataFile, selectedFile); break; + case WebGLCompressionType.None: default: - UnityWebDataHelper.PackFilesToWebData( - unpackedWebDataDirectory, - selectedFile - ); + UnityWebTool.Pack(unpackedWebDataDirectory, selectedFile); break; } - - if ( - webGLCompressionType == WebGLCompressionTypes.Brotli - || webGLCompressionType == WebGLCompressionTypes.GZip - ) - File.Delete(webDataFile); } - - //Directory.Delete(unpackedWebDataDirectory, true); + } + catch (Exception ex) + { + AnsiConsole.MarkupLineInterpolated( + $"[red]( RAWR )[/] Error when compressing Unity Web Data! {ex.Message}" + ); + } + finally + { + if (isWebGL) + { + try + { + if (Directory.Exists(unpackedWebDataDirectory)) + Directory.Delete(unpackedWebDataDirectory, true); + if ( + !webGLCompressionType.Equals(WebGLCompressionType.None) + && File.Exists(webDataFile) + ) + File.Delete(webDataFile); + } + catch { } + } } Console.WriteLine(); @@ -388,43 +334,48 @@ static void PrintHelp() ); Console.WriteLine(); AnsiConsole.MarkupLine( - "USSR is a CLI tool to easily remove Unity splash screen logo (Made with Unity) from your game and keeping your logo displayed. USSR didn't directly \"hack\" Unity Editor, but the generated build." + "USSR is a CLI tool to easily remove Unity splash screen logo (Made with Unity) from your game and keep your logo displayed. USSR didn't directly \"hack\" Unity Editor, but the generated build." ); Console.WriteLine(); AnsiConsole.MarkupLine( - "Before using USSR, make sure you have set splash screen \"Draw Mode\" in Player Settings to \"All Sequential\" and don't forget to backup your game files (USSR by default backuping your game files before doing it\'s job, but might be not because of bugs)." + "Before using USSR, make sure you have set the splash screen [bold green]\"Draw Mode\"[/] in [bold green]Player Settings[/] to [bold green]\"All Sequential\"[/] and don't forget to backup your game files!" ); Console.WriteLine(); AnsiConsole.MarkupLine( "For more information, visit USSR GitHub repo: [link]https://github.com/kiraio-moe/USSR[/]" ); Console.WriteLine(); - AnsiConsole.MarkupLine("[bold green]How to Use[/]"); - AnsiConsole.MarkupLine("Select the Action, find and choose one of these files in you game data:"); - AnsiConsole.MarkupLine("[green]globalgamemanagers[/] | [green]data.unity3d[/] | [green].data[/] | [green].data.br[/] | [green].data.gz[/]"); + AnsiConsole.MarkupLine("[bold green]How to Use[/]:"); + AnsiConsole.MarkupLine( + "Select the Action, find and choose one of these files in you game data:" + ); + AnsiConsole.MarkupLine( + "[green]globalgamemanagers[/] | [green]data.unity3d[/] | [green].data[/] | [green].data.br[/] | [green].data.gz[/] | [green].data.unityweb[/]" + ); } static void LoadClassPackage(AssetsManager assetsManager, string tpkFile) { - try + if (File.Exists(tpkFile)) { - AnsiConsole.MarkupLineInterpolated( - $"( INFO ) Loading class types package: [green]{tpkFile}[/]..." - ); - - if (File.Exists(tpkFile)) - assetsManager.LoadClassPackage(path: tpkFile); - else + try + { AnsiConsole.MarkupLineInterpolated( - $"( ERROR ) TPK file not found: [green]{tpkFile}[/]..." + $"( INFO ) Loading class types package: [green]{tpkFile}[/]..." + ); + assetsManager.LoadClassPackage(path: tpkFile); + } + catch (Exception ex) + { + AnsiConsole.MarkupLine( + $"[red]( RAWR )[/] Error when loading class types package! {ex.Message}" ); + } } - catch (Exception ex) - { - AnsiConsole.MarkupLine( - $"[red]( ERROR )[/] Unable to load class types package! {ex.Message}" + else + AnsiConsole.MarkupLineInterpolated( + $"[red]( RAWR )[/] TPK file not found: [red]{tpkFile}[/]..." ); - } } /// @@ -433,27 +384,36 @@ static void LoadClassPackage(AssetsManager assetsManager, string tpkFile) /// /// static AssetsFileInstance? LoadAssetFileInstance( - string sourceFile, + string assetFile, AssetsManager assetsManager ) { - try + AssetsFileInstance? assetFileInstance = null; + + if (File.Exists(assetFile)) { - if (File.Exists(sourceFile)) - return assetsManager.LoadAssetsFile(sourceFile, true); - else + try { AnsiConsole.MarkupLineInterpolated( - $"[red]( ERROR )[/] File not found: [red]{sourceFile}[/]" + $"( INFO ) Loading asset file: [green]{assetFile}[/]..." + ); + assetFileInstance = assetsManager.LoadAssetsFile(assetFile, true); + } + catch (Exception ex) + { + AnsiConsole.MarkupLineInterpolated( + $"[red]( RAWR )[/] Error when loading asset file! {ex.Message}" ); - return null; } } - catch (Exception ex) + else { - AnsiConsole.WriteException(ex); - return null; + AnsiConsole.MarkupLineInterpolated( + $"[red]( RAWR )[/] Asset file not found: [red]{assetFile}[/]" + ); } + + return assetFileInstance; } /// @@ -464,65 +424,81 @@ AssetsManager assetsManager /// /// static AssetsFileInstance? LoadAssetFileInstance( - string sourceFile, + string assetFile, AssetsManager assetsManager, BundleFileInstance? bundleFileInstance ) { - try + AssetsFileInstance? assetFileInstance = null; + + if (File.Exists(assetFile)) { - if (File.Exists(sourceFile)) - return assetsManager.LoadAssetsFileFromBundle(bundleFileInstance, 0, true); - else + try { AnsiConsole.MarkupLineInterpolated( - $"[red]( ERROR )[/] File not found: [red]{sourceFile}[/]" + $"( INFO ) Loading asset file: [green]{assetFile}[/]..." + ); + assetFileInstance = assetsManager.LoadAssetsFileFromBundle( + bundleFileInstance, + 0, + true + ); + } + catch (Exception ex) + { + AnsiConsole.MarkupLineInterpolated( + $"[red]( RAWR )[/] Error when loading asset file! {ex.Message}" ); - return null; } } - catch (Exception ex) + else { - AnsiConsole.WriteException(ex, ExceptionFormats.ShowLinks); - return null; + AnsiConsole.MarkupLineInterpolated( + $"[red]( RAWR )[/] Asset file not found: [red]{assetFile}[/]" + ); } + + return assetFileInstance; } static BundleFileInstance? LoadBundleFileInstance( - string sourceFile, + string bundleFile, AssetsManager assetsManager, - FileStream? unpackedSourceFileStream + FileStream? unpackedBundleFileStream ) { - try + BundleFileInstance? bundleFileInstance = null; + + if (File.Exists(bundleFile)) { - if (File.Exists(sourceFile)) + try { - BundleFileInstance bundleFileInstance = assetsManager.LoadBundleFile( - sourceFile, - false + AnsiConsole.MarkupLineInterpolated( + $"( INFO ) Loading bundle file: [green]{bundleFile}[/]..." ); - unpackedSourceFileStream = File.Open($"{sourceFile}.unpacked", FileMode.Create); + bundleFileInstance = assetsManager.LoadBundleFile(bundleFile, false); + // It will throw an error if we use 'using' + unpackedBundleFileStream = File.Open($"{bundleFile}.unpacked", FileMode.Create); bundleFileInstance.file = BundleHelper.UnpackBundleToStream( bundleFileInstance.file, - unpackedSourceFileStream + unpackedBundleFileStream ); - - return bundleFileInstance; } - else + catch (Exception ex) { AnsiConsole.MarkupLineInterpolated( - $"[red]( ERROR )[/] File not found: [red]{sourceFile}[/]" + $"[red]( RAWR )[/] Error when loading bundle file! {ex.Message}" ); - return null; } } - catch (Exception ex) + else { - AnsiConsole.WriteException(ex); - return null; + AnsiConsole.MarkupLineInterpolated( + $"[red]( RAWR )[/] Bundle file not found: [red]{bundleFile}[/]" + ); } + + return bundleFileInstance; } static List? RemoveSplashScreen( @@ -530,96 +506,133 @@ AssetsManager assetsManager AssetsFileInstance? assetFileInstance ) { - AssetsFile? assetFile = assetFileInstance?.file; + try + { + AnsiConsole.MarkupLine("( INFO ) Start removing Unity splash screen..."); - List? buildSettingsInfo = assetFile?.GetAssetsOfType( - AssetClassID.BuildSettings - ); - AssetTypeValueField buildSettingsBase = assetsManager.GetBaseField( - assetFileInstance, - buildSettingsInfo?[0] - ); + AssetsFile? assetFile = assetFileInstance?.file; - List? playerSettingsInfo = assetFile?.GetAssetsOfType( - AssetClassID.PlayerSettings - ); - AssetTypeValueField playerSettingsBase = assetsManager.GetBaseField( - assetFileInstance, - playerSettingsInfo?[0] - ); + List? buildSettingsInfo = assetFile?.GetAssetsOfType( + AssetClassID.BuildSettings + ); + AssetTypeValueField buildSettingsBase = assetsManager.GetBaseField( + assetFileInstance, + buildSettingsInfo?[0] + ); - // Required fields to remove splash screen - bool hasProVersion = buildSettingsBase["hasPROVersion"].AsBool; - bool showUnityLogo = playerSettingsBase["m_ShowUnitySplashLogo"].AsBool; + List? playerSettingsInfo = assetFile?.GetAssetsOfType( + AssetClassID.PlayerSettings + ); + AssetTypeValueField? playerSettingsBase = assetsManager.GetBaseField( + assetFileInstance, + playerSettingsInfo?[0] + ); - AnsiConsole.MarkupLine("( INFO ) Removing Unity splash screen..."); + if (playerSettingsBase == null) + { + AnsiConsole.MarkupLine( + "[red]( RAWR )[/] Can\'t get Player Settings fields! It\'s possible that this current version of Unity are currently not supported yet." + ); + AnsiConsole.MarkupLine( + "Try updating USSR [bold green]classdata.tpk[/] manually from there: [link green]https://nightly.link/AssetRipper/Tpk/workflows/type_tree_tpk/master/uncompressed_file.zip[/] and try again." + ); + AnsiConsole.MarkupLine( + "If the issue still persist, try switching to another Unity version." + ); + return null; + } + + // Required fields to remove splash screen + bool hasProVersion = buildSettingsBase["hasPROVersion"].AsBool; + bool showUnityLogo = playerSettingsBase["m_ShowUnitySplashLogo"].AsBool; + + // Check if the splash screen have been removed + if (hasProVersion && !showUnityLogo) + { + AnsiConsole.MarkupLine( + "[yellow]( WARN ) Unity splash screen already removed![/]" + ); + return null; + } + + AssetTypeValueField splashScreenLogos = playerSettingsBase[ + "m_SplashScreenLogos.Array" + ]; + int totalSplashScreen = splashScreenLogos.Count(); + int splashScreenIndex = 0; + + AnsiConsole.MarkupLineInterpolated( + $"( INFO ) There's [green]{totalSplashScreen}[/] splash screen detected." + ); + + switch (totalSplashScreen) + { + case 0: + AnsiConsole.MarkupLine( + "[yellow]( WARN ) Nothing to do. Finally, taking a rest :)[/]" + ); + return null; + case 1: + AnsiConsole.MarkupLine("( INFO ) Auto remove the splash screen..."); + goto RemoveSplashScreen; // auto remove the splash screen + } - // Check if the splash screen have been removed - if (hasProVersion && !showUnityLogo) - { AnsiConsole.MarkupLine( - "[yellow]( WARN ) Unity splash screen have been removed![/]" + "What order are Unity splash screen logo in your Player Settings? (Start from 0 [upmost])" ); - return null; - } - // AnsiConsole.MarkupLine( - // "( INFO ) Sometimes USSR [yellow]can\'t automatically detect Unity splash screen logo[/] and it\'s leading to accidentally [red]removing your own logo[/]. To tackle this, USSR [green]need information about \"Made With Unity\" logo duration[/]." - // ); - // AnsiConsole.MarkupLine( - // "( INFO ) Please [red]make a difference with the logo duration[/] when you build your game! [red]If your logo and Unity logo have same duration, USSR will remove both of them[/]. If no value provided, USSR will use it\'s own way to detect it and [red]may removing your own logo[/]." - // ); - // AnsiConsole.Markup("[green](Optional)[/] Enter Unity splash screen logo duration: "); - - // int unityLogoDuration = 0; - // try - // { - // int.TryParse( - // Console.ReadLine(), - // System.Globalization.NumberStyles.Integer, - // null, - // out unityLogoDuration - // ); - // } - // catch (Exception ex) - // { - // AnsiConsole.WriteException(ex); - // } + InputLogoIndex: + int.TryParse( + Console.ReadLine(), + System.Globalization.NumberStyles.Integer, + null, + out splashScreenIndex + ); - AnsiConsole.MarkupLineInterpolated( - $"( INFO ) Set [green]hasProVersion = {!hasProVersion}[/] | [green]m_ShowUnitySplashLogo = {!showUnityLogo}[/]" - ); + if (splashScreenIndex < 0 && splashScreenIndex >= totalSplashScreen) + { + AnsiConsole.MarkupLineInterpolated( + $"[red]( RAWR )[/] There's no splash screen at index [red]{splashScreenIndex}[/]! Try again." + ); + goto InputLogoIndex; + } - // Remove Unity splash screen by flipping these boolean fields - buildSettingsBase["hasPROVersion"].AsBool = !hasProVersion; // true - playerSettingsBase["m_ShowUnitySplashLogo"].AsBool = !showUnityLogo; // false - - AssetTypeValueField splashScreenLogos = playerSettingsBase["m_SplashScreenLogos.Array"]; - AnsiConsole.MarkupLineInterpolated($"( INFO ) There's [green]{splashScreenLogos.Count()}[/] splash screen detected. What order are Unity splash screen logo in your Player Settings? (Start from 0)"); - int.TryParse( - Console.ReadLine(), - System.Globalization.NumberStyles.Integer, - null, - out int unitySplashIndex - ); + RemoveSplashScreen: + AnsiConsole.MarkupLineInterpolated( + $"( INFO ) Set [green]hasProVersion[/] = [green]{!hasProVersion}[/] | [green]m_ShowUnitySplashLogo[/] = [green]{!showUnityLogo}[/]" + ); - InputLogoIndex: - if (unitySplashIndex < 0 && unitySplashIndex > splashScreenLogos.Count()) - { - AnsiConsole.MarkupLineInterpolated($"( ERROR ) There's no logo at index {unitySplashIndex}! Try again!"); - goto InputLogoIndex; - } + // Remove Unity splash screen by flipping these boolean fields + buildSettingsBase["hasPROVersion"].AsBool = !hasProVersion; // true + playerSettingsBase["m_ShowUnitySplashLogo"].AsBool = !showUnityLogo; // false - AnsiConsole.MarkupLineInterpolated( - $"( INFO ) [green]Removing Unity splash screen at index {unitySplashIndex}.[/]" - ); - splashScreenLogos?.Children.RemoveAt(unitySplashIndex); + AnsiConsole.MarkupLineInterpolated( + $"( INFO ) [green]Splash screen removed at index {splashScreenIndex}.[/]" + ); + + splashScreenLogos?.Children.RemoveAt(splashScreenIndex); - return new() + return new() + { + new AssetsReplacerFromMemory( + assetFile, + buildSettingsInfo?[0], + buildSettingsBase + ), + new AssetsReplacerFromMemory( + assetFile, + playerSettingsInfo?[0], + playerSettingsBase + ) + }; + } + catch (Exception ex) { - new AssetsReplacerFromMemory(assetFile, buildSettingsInfo?[0], buildSettingsBase), - new AssetsReplacerFromMemory(assetFile, playerSettingsInfo?[0], playerSettingsBase) - }; + AnsiConsole.MarkupLineInterpolated( + $"[red]( RAWR )[/] Error when removing the splash screen! {ex.Message}" + ); + return null; + } } static List? RemoveWatermark( @@ -652,8 +665,8 @@ AssetsManager assetsManager AnsiConsole.MarkupLineInterpolated( $"( INFO ) Set [green]isNoWatermarkBuild = {!noWatermark}[/] | [green]isTrial = {!isTrial}[/]" ); - buildSettingsBase["isNoWatermarkBuild"].AsBool = !noWatermark; - buildSettingsBase["isTrial"].AsBool = !isTrial; + buildSettingsBase["isNoWatermarkBuild"].AsBool = true; + buildSettingsBase["isTrial"].AsBool = false; AnsiConsole.MarkupLine("( INFO ) [green]Watermark successfully removed.[/]"); return new() @@ -668,7 +681,7 @@ AssetsManager assetsManager catch (Exception ex) { AnsiConsole.MarkupLineInterpolated( - $"( ERROR ) Unable to remove watermark! {ex.Message}" + $"[red]( RAWR )[/] Error when removing the watermark! {ex.Message}" ); return null; } @@ -739,12 +752,66 @@ List assetsReplacer } catch (Exception ex) { - AnsiConsole.WriteException(ex); - return; + AnsiConsole.MarkupLineInterpolated( + $"[red]( RAWR )[/] Error when writing changes! {ex.Message}" + ); } finally { - File.Delete(uncompressedBundleFile); + if (File.Exists(uncompressedBundleFile)) + File.Delete(uncompressedBundleFile); + } + } + + static int DecompressCompressedWebData( + WebGLCompressionType compressionType, + string inputPath, + string outputPath + ) + { + switch (compressionType) + { + case WebGLCompressionType.Brotli: + try + { + AnsiConsole.MarkupLineInterpolated( + $"( INFO ) Decompressing Brotli [green]{inputPath}[/]..." + ); + BrotliUtils.DecompressFile(inputPath, outputPath); + return 0; + } + catch (Exception ex) + { + AnsiConsole.MarkupLineInterpolated( + $"[red]( RAWR ) Failed to decompress {inputPath}![/] {ex.Message} Try choose different compression type." + ); + Console.WriteLine(); + if (File.Exists(outputPath)) + File.Delete(outputPath); + return 1; + } + case WebGLCompressionType.GZip: + try + { + AnsiConsole.MarkupLineInterpolated( + $"( INFO ) Decompressing GZip [green]{inputPath}[/]..." + ); + GZipUtils.DecompressFile(inputPath, outputPath); + return 0; + } + catch (Exception ex) + { + AnsiConsole.MarkupLineInterpolated( + $"[red]( RAWR ) Failed to decompress {inputPath}![/] {ex.Message} Try choose different compression type." + ); + Console.WriteLine(); + if (File.Exists(outputPath)) + File.Delete(outputPath); + return 1; + } + case WebGLCompressionType.None: + default: + return 1; } } } diff --git a/USSR.csproj b/USSR.csproj index 07cc566..66c06bc 100644 --- a/USSR.csproj +++ b/USSR.csproj @@ -11,6 +11,7 @@ + diff --git a/UnityWebData.cs b/UnityWebData.cs deleted file mode 100644 index 9618986..0000000 --- a/UnityWebData.cs +++ /dev/null @@ -1,146 +0,0 @@ -// This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild - -namespace Kaitai -{ - public partial class UnityWebData : KaitaiStruct - { - public static UnityWebData FromFile(string fileName) - { - return new UnityWebData(new KaitaiStream(fileName)); - } - - public UnityWebData( - KaitaiStream p__io, - KaitaiStruct p__parent = null, - UnityWebData p__root = null - ) : base(p__io) - { - m_parent = p__parent; - m_root = p__root ?? this; - _read(); - } - - private void _read() - { - _magic = System.Text.Encoding - .GetEncoding("utf-8") - .GetString(m_io.ReadBytesTerm(0, false, true, true)); - _beginOffset = m_io.ReadU4le(); - _files = new List(); - { - var i = 0; - FileEntry M_; - do - { - M_ = new FileEntry(m_io, this, m_root); - _files.Add(M_); - i++; - } while (!(M_Io.Pos == BeginOffset)); - } - } - - public partial class FileEntry : KaitaiStruct - { - public static FileEntry FromFile(string fileName) - { - return new FileEntry(new KaitaiStream(fileName)); - } - - public FileEntry( - KaitaiStream p__io, - UnityWebData p__parent = null, - UnityWebData p__root = null - ) : base(p__io) - { - m_parent = p__parent; - m_root = p__root; - f_data = false; - _read(); - } - - private void _read() - { - _fileOffset = m_io.ReadU4le(); - _fileSize = m_io.ReadU4le(); - _filenameSize = m_io.ReadU4le(); - _filename = System.Text.Encoding - .GetEncoding("utf-8") - .GetString(m_io.ReadBytes(FilenameSize)); - } - - private bool f_data; - private byte[] _data; - public byte[] Data - { - get - { - if (f_data) - return _data; - KaitaiStream io = M_Root.M_Io; - long _pos = io.Pos; - io.Seek(FileOffset); - _data = io.ReadBytes(FileSize); - io.Seek(_pos); - f_data = true; - return _data; - } - } - private uint _fileOffset; - private uint _fileSize; - private uint _filenameSize; - private string _filename; - private UnityWebData m_root; - private UnityWebData m_parent; - public uint FileOffset - { - get { return _fileOffset; } - } - public uint FileSize - { - get { return _fileSize; } - } - public uint FilenameSize - { - get { return _filenameSize; } - } - public string Filename - { - get { return _filename; } - } - public UnityWebData M_Root - { - get { return m_root; } - } - public UnityWebData M_Parent - { - get { return m_parent; } - } - } - - private string _magic; - private uint _beginOffset; - private List _files; - private UnityWebData m_root; - private KaitaiStruct m_parent; - public string Magic - { - get { return _magic; } - } - public uint BeginOffset - { - get { return _beginOffset; } - } - public List Files - { - get { return _files; } - } - public UnityWebData M_Root - { - get { return m_root; } - } - public KaitaiStruct M_Parent - { - get { return m_parent; } - } - } -} diff --git a/UnityWebDataHelper.cs b/UnityWebDataHelper.cs deleted file mode 100644 index 3c80535..0000000 --- a/UnityWebDataHelper.cs +++ /dev/null @@ -1,246 +0,0 @@ -using System.Text; -using Kaitai; -using Spectre.Console; - -namespace USSR.Utilities -{ - struct WebData - { - public byte[] Magic; - public uint FirstFileOffset; - public List FileEntries; - public List FileContents; - } - - struct FileEntry - { - public uint FileOffset; - public uint FileSize; - public uint FileNameSize; - public byte[] Name; - } - - internal class UnityWebDataHelper - { - const string MAGIC_HEADER = "UnityWebData1.0"; - - /// - /// Unpack UnityWebData (*.data) to File. - /// - /// - /// Output directory with as the name. - internal static string UnpackWebDataToFile(string? bundleFile) - { - AnsiConsole.MarkupLineInterpolated($"( INFO ) Extracting [green]{bundleFile}[/]..."); - - if (!File.Exists(bundleFile)) - { - AnsiConsole.MarkupLineInterpolated( - $"[red]( ERROR )[/] File not found: [red]{bundleFile}[/]" - ); - return string.Empty; - } - - try - { - // Create the Kaitai stream and the root object from the parsed data - UnityWebData? unityWebData = UnityWebData.FromFile(bundleFile); - - string? outputDirectory = Path.Combine( - Path.GetDirectoryName(bundleFile) ?? string.Empty, - Path.GetFileNameWithoutExtension(bundleFile) - ); - - if (!Directory.Exists(outputDirectory)) - Directory.CreateDirectory(outputDirectory); - - foreach (UnityWebData.FileEntry fileEntry in unityWebData.Files) - { - string? fileName = fileEntry?.Filename ?? string.Empty; - - // Create file entry directory - string? fileNameDirectory = Path.Combine( - outputDirectory, - Path.GetDirectoryName(fileName) ?? string.Empty - ); - if (!Directory.Exists(fileNameDirectory)) - Directory.CreateDirectory(fileNameDirectory); - - string? outputFile = Path.Combine(outputDirectory, fileName); - - using FileStream? outputFileStream = new(outputFile, FileMode.Create); - outputFileStream?.Write(fileEntry?.Data); - } - - return outputDirectory; - } - catch (Exception ex) - { - AnsiConsole.MarkupLineInterpolated($"( ERROR ) Failed to extract! {ex.Message}"); - return string.Empty; - } - } - - /// - /// Pack a folder as UnityWebData. - /// - /// - /// - internal static string PackFilesToWebData(string sourceFolder, string outputFile) - { - AnsiConsole.MarkupLineInterpolated( - $"( INFO ) Packing [green]{sourceFolder}[/] as [green]{outputFile}[/]..." - ); - - if (!Directory.Exists(sourceFolder)) - { - AnsiConsole.MarkupLineInterpolated( - $"[red]( ERROR )[/] Directory not found: [red]{sourceFolder}[/]" - ); - return string.Empty; - } - - try - { - // Get all files recursively - List files = Directory - .GetFiles(sourceFolder, "*.*", SearchOption.AllDirectories) - .ToList(); - - // Get files in root directory - List rootFolderFiles = files - .Where(f => Path.GetDirectoryName(f) == sourceFolder) - .ToList(); - - // Get files inside subdirectories - List subdirectoryFiles = files.Except(rootFolderFiles).ToList(); - - // Sort the subdirectory files in descending order - subdirectoryFiles.Sort((a, b) => b.CompareTo(a)); - - // Combine the lists and print the result - files = subdirectoryFiles.Concat(rootFolderFiles).ToList(); - List? filesName = new(); - - foreach (string file in files) - filesName.Add( - file.Replace(sourceFolder, "") - .Trim(Path.DirectorySeparatorChar) - .Replace(@"\", @"/") - ); - - using MemoryStream tempStream = new(); - using BinaryWriter tempWriter = new(tempStream); - byte[] magic = AddNullTerminate(Encoding.UTF8.GetBytes(MAGIC_HEADER)); - List fileEntries = new(); - List fileContents = new(); - List fileOffsetValues = new(); - List fileOffsetEntryPosition = new(); - - // Collect file entries - for (int i = 0; i < files.Count; i++) - { - FileInfo fileInfo = new(files[i]); - byte[] fileNameBytes = Encoding.UTF8.GetBytes(filesName[i]); - - fileEntries.Add( - new FileEntry - { - FileOffset = 0, - FileSize = (uint)fileInfo.Length, - FileNameSize = (uint)fileNameBytes.Length, - Name = fileNameBytes - } - ); - - fileContents.Add(File.ReadAllBytes(files[i])); - } - - WebData webData = - new() - { - Magic = magic, - FirstFileOffset = 0, - FileEntries = fileEntries, - FileContents = fileContents - }; - - // Write Magic bytes - tempWriter.Write(webData.Magic); - - // Write a placeholder for FirstFileOffset - fileOffsetEntryPosition.Add(tempStream.Position); - tempWriter.Write(webData.FirstFileOffset); - - // Write each FileEntry - foreach (FileEntry entry in webData.FileEntries) - { - // Write FileOffset - fileOffsetEntryPosition.Add(tempStream.Position); - tempWriter.Write(entry.FileOffset); - - // Write FileSize - tempWriter.Write(entry.FileSize); - - // Write FileNameSize - tempWriter.Write(entry.FileNameSize); - - // Write Name bytes - tempWriter.Write(entry.Name); - } - - foreach (byte[] content in webData.FileContents) - { - // Add current offset to a list to be used later - fileOffsetValues.Add(tempStream.Position); - - // Write the actual data - tempWriter.Write(content); - } - - // Go back to WebData.FirstFileOffset and write the first file offset - tempStream.Seek(fileOffsetEntryPosition[0], SeekOrigin.Begin); - tempWriter.Write((uint)fileOffsetValues[0]); - - // Go back to each FileEntry.FileOffset and write the file offset - for (int i = 0; i < fileOffsetValues.Count; i++) - { - tempStream.Seek(fileOffsetEntryPosition[i + 1], SeekOrigin.Begin); - tempWriter.Write((uint)fileOffsetValues[i]); - } - - // Now write the entire contents of the temporary stream to the actual file - using FileStream fileStream = new(outputFile, FileMode.Create); - tempStream.WriteTo(fileStream); - - return outputFile; - } - catch (Exception ex) - { - AnsiConsole.MarkupLineInterpolated( - $"[red]( ERROR )[/] Failed to pack! {ex.Message}" - ); - return string.Empty; - } - } - - /// - /// Add null terminator at the end of bytes. - /// - /// - /// New array of bytes. - static byte[] AddNullTerminate(byte[] originalArray) - { - // Create a new array with one extra element to accommodate the null byte - byte[] newArray = new byte[originalArray.Length + 1]; - - // Copy the original array to the new array - Array.Copy(originalArray, newArray, originalArray.Length); - - // Set the last element to be the null byte - newArray[^1] = 0; - - return newArray; - } - } -} diff --git a/BrotliUtils.cs b/Utils/BrotliUtils.cs similarity index 67% rename from BrotliUtils.cs rename to Utils/BrotliUtils.cs index aceb716..abed1ac 100644 --- a/BrotliUtils.cs +++ b/Utils/BrotliUtils.cs @@ -49,11 +49,21 @@ internal static byte[] DecompressBytes(byte[] bytes) internal static string DecompressFile(string compressedFileName, string outputFileName) { - using FileStream compressedFileStream = File.Open(compressedFileName, FileMode.Open); - using FileStream outputFileStream = File.Create(outputFileName); - DecompressStream(compressedFileStream, outputFileStream); + try + { + using FileStream compressedFileStream = File.Open( + compressedFileName, + FileMode.Open + ); + using FileStream outputFileStream = File.Create(outputFileName); + DecompressStream(compressedFileStream, outputFileStream); - return outputFileName; + return outputFileName; + } + catch (Exception ex) + { + throw new Exception(ex.Message); + } } internal static void DecompressStream(Stream compressedStream, Stream outputStream) @@ -62,24 +72,24 @@ internal static void DecompressStream(Stream compressedStream, Stream outputStre decompressor.CopyTo(outputStream); } - internal static void WriteUnityIdentifier(string filePath, byte[] magicBytes) - { - try - { - using ( - FileStream fileStream = new FileStream( - filePath, - FileMode.Open, - FileAccess.ReadWrite - ) - ) - { - fileStream.Seek(0, SeekOrigin.Begin); - fileStream.Write(magicBytes, 0, magicBytes.Length); - } - } - catch (Exception) { } - } + // internal static void WriteUnityIdentifier(string filePath, byte[] magicBytes) + // { + // try + // { + // using ( + // FileStream fileStream = new FileStream( + // filePath, + // FileMode.Open, + // FileAccess.ReadWrite + // ) + // ) + // { + // fileStream.Seek(0, SeekOrigin.Begin); + // fileStream.Write(magicBytes, 0, magicBytes.Length); + // } + // } + // catch { } + // } } } diff --git a/GZipUtils.cs b/Utils/GZipUtils.cs similarity index 81% rename from GZipUtils.cs rename to Utils/GZipUtils.cs index 02df605..7d28d53 100644 --- a/GZipUtils.cs +++ b/Utils/GZipUtils.cs @@ -49,11 +49,21 @@ internal static byte[] DecompressBytes(byte[] bytes) internal static string DecompressFile(string compressedFileName, string outputFileName) { - using FileStream compressedFileStream = File.Open(compressedFileName, FileMode.Open); - using FileStream outputFileStream = File.Create(outputFileName); - DecompressStream(compressedFileStream, outputFileStream); + try + { + using FileStream compressedFileStream = File.Open( + compressedFileName, + FileMode.Open + ); + using FileStream outputFileStream = File.Create(outputFileName); + DecompressStream(compressedFileStream, outputFileStream); - return outputFileName; + return outputFileName; + } + catch (Exception ex) + { + throw new Exception(ex.Message); + } } internal static void DecompressStream(Stream compressedStream, Stream outputStream) diff --git a/Utility.cs b/Utils/Utility.cs similarity index 90% rename from Utility.cs rename to Utils/Utility.cs index ea42315..cc478cd 100644 --- a/Utility.cs +++ b/Utils/Utility.cs @@ -69,7 +69,7 @@ internal static string CloneFile(string sourceFile, string outputFile) } /// - /// Backup a fileas ".bak". If it's already exist, skip. + /// Backup a file as ".bak". If it's already exist, skip. /// /// /// @@ -92,20 +92,19 @@ internal static string BackupOnlyOnce(string sourceFile) /// Delete . /// /// - internal static void CleanUp(List? paths) + internal static void CleanUp(List paths) { - if (paths == null && paths?.Count < 1) - return; - - AnsiConsole.MarkupLine("( INFO ) Cleaning up temporary files..."); - - foreach (string path in paths) + if (paths != null && paths?.Count > 0) { - if (File.Exists(path)) - File.Delete(path); + AnsiConsole.MarkupLine("( INFO ) Cleaning up temporary files..."); + foreach (string path in paths) + { + if (File.Exists(path)) + File.Delete(path); - if (Directory.Exists(path)) - Directory.Delete(path, true); + if (Directory.Exists(path)) + Directory.Delete(path, true); + } } } diff --git a/classdata.tpk b/classdata.tpk index 8bb82f5..42a5911 100644 Binary files a/classdata.tpk and b/classdata.tpk differ