From 92039decc465c4c4864bc704e5d0d64121e0aef2 Mon Sep 17 00:00:00 2001 From: Bayu Satiyo Date: Sat, 25 Nov 2023 18:19:34 +0700 Subject: [PATCH 1/5] Refactor remove splash screen functionality & add build for .NET 8 Signed-off-by: Bayu Satiyo --- USSR.cs | 128 ++++++++++++++++++---------------------------------- USSR.csproj | 2 +- 2 files changed, 45 insertions(+), 85 deletions(-) diff --git a/USSR.cs b/USSR.cs index a58a98d..a98fda0 100644 --- a/USSR.cs +++ b/USSR.cs @@ -374,7 +374,7 @@ static void Main(string[] args) File.Delete(webDataFile); } - Directory.Delete(unpackedWebDataDirectory, true); + //Directory.Delete(unpackedWebDataDirectory, true); } Console.WriteLine(); @@ -400,7 +400,7 @@ static void PrintHelp() ); Console.WriteLine(); AnsiConsole.MarkupLine("[bold green]How to Use[/]"); - AnsiConsole.MarkupLine("Select the Action, find and choose one of this files in you game data:"); + 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[/]"); } @@ -563,31 +563,31 @@ AssetsManager assetsManager 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); - } + // 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); + // } AnsiConsole.MarkupLineInterpolated( - $"( INFO ) Set hasProVersion = [green]{!hasProVersion}[/] | m_ShowUnitySplashLogo = [green]{!showUnityLogo}[/]" + $"( INFO ) Set [green]hasProVersion = {!hasProVersion}[/] | [green]m_ShowUnitySplashLogo = {!showUnityLogo}[/]" ); // Remove Unity splash screen by flipping these boolean fields @@ -595,68 +595,25 @@ out unityLogoDuration playerSettingsBase["m_ShowUnitySplashLogo"].AsBool = !showUnityLogo; // false AssetTypeValueField splashScreenLogos = playerSettingsBase["m_SplashScreenLogos.Array"]; - AssetTypeValueField? unityLogo = null; - - // Iterate over "m_SplashScreenLogos" to find Unity splash screen logo - foreach (AssetTypeValueField splashLogo in splashScreenLogos) - { - // Get the Sprite asset - AssetTypeValueField? logoPointer = splashLogo?["logo"]; - // Get the external asset - AssetExternal logoExtInfo = assetsManager.GetExtAsset( - assetFileInstance, - logoPointer - ); - - /* - * We have 2 ways to detect the Unity splash screen logo. - * 1. Check if the base field isn't null. This method guaranteed to be 100% work. - * 2. Use optional input value from user to determine the splash screen. - */ - - if (logoExtInfo.baseField != null) - { - // Get the base field - AssetTypeValueField? logoBase = logoExtInfo.baseField; - string? logoName = logoBase["m_Name"].AsString; - - // If it's Unity splash screen logo - unityLogo = logoName.Contains("UnitySplash-cube") ? splashLogo : null; - } - else - { - /* - * Sometimes AssetsTools won't load "UnitySplash-cube" - * external asset while in Bundle file. - * - * Luckily, we can still find it by using the logo duration. - */ - unityLogo = - splashLogo?["duration"].AsInt == unityLogoDuration ? splashLogo : null; - } - } + 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 + ); - if (unityLogo == null && unityLogoDuration <= 0) + InputLogoIndex: + if (unitySplashIndex < 0 && unitySplashIndex > splashScreenLogos.Count()) { - AnsiConsole.MarkupLine( - "[red]( ERROR )[/] Failed to remove Unity splash screen logo!" - ); - AnsiConsole.MarkupLine( - "[red]( ERROR )[/] Looks like USSR [red]can\'t detect the Unity splash screen[/] and at the same time [red]you didn\'t provide any value to the input[/] to help USSR find the splash screen. [yellow]Try again and fill in the input.[/]" - ); - return null; + AnsiConsole.MarkupLineInterpolated($"( ERROR ) There's no logo at index {unitySplashIndex}! Try again!"); + goto InputLogoIndex; } - /* - * Remove "UnitySplash-cube" to completely remove - * Unity splash screen logo. - */ - AnsiConsole.MarkupLineInterpolated($"( INFO ) Removing [red]UnitySplash-cube[/]..."); - splashScreenLogos?.Children.Remove(unityLogo); - - AnsiConsole.MarkupLine( - "( INFO ) [green]Successfully removed the Unity splash screen.[/]" + AnsiConsole.MarkupLineInterpolated( + $"( INFO ) [green]Removing Unity splash screen at index {unitySplashIndex}.[/]" ); + splashScreenLogos?.Children.RemoveAt(unitySplashIndex); return new() { @@ -684,16 +641,19 @@ out unityLogoDuration ); bool noWatermark = buildSettingsBase["isNoWatermarkBuild"].AsBool; - if (noWatermark) + bool isTrial = buildSettingsBase["isTrial"].AsBool; + + if (noWatermark && !isTrial) { AnsiConsole.MarkupLine("[yellow]( WARN ) Watermark have been removed![/]"); return null; } AnsiConsole.MarkupLineInterpolated( - $"( INFO ) Set isNoWatermarkBuild = [green]{!noWatermark}[/]" + $"( INFO ) Set [green]isNoWatermarkBuild = {!noWatermark}[/] | [green]isTrial = {!isTrial}[/]" ); buildSettingsBase["isNoWatermarkBuild"].AsBool = !noWatermark; + buildSettingsBase["isTrial"].AsBool = !isTrial; AnsiConsole.MarkupLine("( INFO ) [green]Watermark successfully removed.[/]"); return new() diff --git a/USSR.csproj b/USSR.csproj index ef4950b..07cc566 100644 --- a/USSR.csproj +++ b/USSR.csproj @@ -3,7 +3,7 @@ Exe win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64 - net6.0;net7.0 + net6.0;net7.0;net8.0 enable enable From 120ee197d55a8a927a2a3542bc377fb30e5f42b2 Mon Sep 17 00:00:00 2001 From: Bayu Satiyo Date: Wed, 29 Nov 2023 15:17:25 +0700 Subject: [PATCH 2/5] Refactor error handling - Auto remove splash screen if there's one. Signed-off-by: Bayu Satiyo --- USSR.cs | 422 +++++++++++++++++++++++++++++++--------------------- USSR.csproj | 1 + 2 files changed, 256 insertions(+), 167 deletions(-) diff --git a/USSR.cs b/USSR.cs index af3fb53..068404a 100644 --- a/USSR.cs +++ b/USSR.cs @@ -73,8 +73,9 @@ public class USSR 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 + // I have already test this magic bytes into an asset file, but + // it's turning out that it's not work. static readonly byte[] unityBrotliMagic = { 0x6B, @@ -126,13 +127,14 @@ enum AssetTypes static AssetTypes assetType; - enum WebGLCompressionTypes + enum WebGLCompressionType { + None, Brotli, GZip } - static WebGLCompressionTypes webGLCompressionType; + static WebGLCompressionType webGLCompressionType; static void Main(string[] args) { @@ -208,11 +210,14 @@ static void Main(string[] args) isWebGL = true; webDataFile = selectedFile; } - else if (Utility.ValidateFile(selectedFile, unityBrotliMagic) || Path.GetExtension(selectedFile) == ".br") + else if ( + Utility.ValidateFile(selectedFile, unityBrotliMagic) + || Path.GetExtension(selectedFile) == ".br" + ) { AnsiConsole.MarkupLine("( INFO ) [green]UnityWebData Brotli[/] file selected."); isWebGL = true; - webGLCompressionType = WebGLCompressionTypes.Brotli; + webGLCompressionType = WebGLCompressionType.Brotli; AnsiConsole.MarkupLineInterpolated( $"( INFO ) Decompressing [green]{selectedFile}[/]..." @@ -223,7 +228,7 @@ static void Main(string[] args) { AnsiConsole.MarkupLine("( INFO ) [green]UnityWebData GZip[/] file selected."); isWebGL = true; - webGLCompressionType = WebGLCompressionTypes.GZip; + webGLCompressionType = WebGLCompressionType.GZip; AnsiConsole.MarkupLineInterpolated( $"( INFO ) Decompressing [green]{selectedFile}[/]..." @@ -263,6 +268,7 @@ static void Main(string[] args) AssetsFileInstance? assetFileInstance = null; BundleFileInstance? bundleFileInstance = null; FileStream? bundleStream = null; + List? assetsReplacer = null; string tempFile = Utility.CloneFile(inspectedFile, $"{inspectedFile}.temp"); temporaryFiles.Add(tempFile); @@ -287,20 +293,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]( ERROR )[/] 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,21 +337,18 @@ 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 - ) + if (webGLCompressionType != WebGLCompressionType.None) UnityWebDataHelper.PackFilesToWebData( unpackedWebDataDirectory, webDataFile @@ -346,19 +356,20 @@ static void Main(string[] args) switch (webGLCompressionType) { - case WebGLCompressionTypes.Brotli: + case WebGLCompressionType.Brotli: 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: 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, @@ -367,14 +378,20 @@ static void Main(string[] args) break; } - if ( - webGLCompressionType == WebGLCompressionTypes.Brotli - || webGLCompressionType == WebGLCompressionTypes.GZip - ) + if (webGLCompressionType != WebGLCompressionType.None) File.Delete(webDataFile); } - - //Directory.Delete(unpackedWebDataDirectory, true); + } + catch (Exception ex) + { + AnsiConsole.MarkupLineInterpolated( + $"[red]( ERROR )[/] Error when compressing Unity Web Data! {ex.Message}" + ); + } + finally + { + if (Directory.Exists(unpackedWebDataDirectory)) + Directory.Delete(unpackedWebDataDirectory, true); } Console.WriteLine(); @@ -388,43 +405,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[/]" + ); } 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]( ERROR )[/] 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]( ERROR )[/] TPK file not found: [red]{tpkFile}[/]..." ); - } } /// @@ -433,27 +455,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]( ERROR )[/] Error when loading asset file! {ex.Message}" ); - return null; } } - catch (Exception ex) + else { - AnsiConsole.WriteException(ex); - return null; + AnsiConsole.MarkupLineInterpolated( + $"[red]( ERROR )[/] Asset file not found: [red]{assetFile}[/]" + ); } + + return assetFileInstance; } /// @@ -464,65 +495,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]( ERROR )[/] Error when loading asset file! {ex.Message}" ); - return null; } } - catch (Exception ex) + else { - AnsiConsole.WriteException(ex, ExceptionFormats.ShowLinks); - return null; + AnsiConsole.MarkupLineInterpolated( + $"[red]( ERROR )[/] 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]( ERROR )[/] Error when loading bundle file! {ex.Message}" ); - return null; } } - catch (Exception ex) + else { - AnsiConsole.WriteException(ex); - return null; + AnsiConsole.MarkupLineInterpolated( + $"[red]( ERROR )[/] Bundle file not found: [red]{bundleFile}[/]" + ); } + + return bundleFileInstance; } static List? RemoveSplashScreen( @@ -530,96 +577,135 @@ 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]( ERROR )[/] 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[/] 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 have been 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]Did you set the splash screen [bold]Draw Mode[/] to [bold]Unity Logo Below[/]? That\'s useless..[/]" + ); + 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)" ); - 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]( ERROR )[/] 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 = {!hasProVersion}[/] | [green]m_ShowUnitySplashLogo = {!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); + if (totalSplashScreen > 0) + { + AnsiConsole.MarkupLineInterpolated( + $"( INFO ) [green]Removed splash screen 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]( ERROR )[/] Error when removing the splash screen! {ex.Message}" + ); + return null; + } } static List? RemoveWatermark( @@ -652,8 +738,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 +754,7 @@ AssetsManager assetsManager catch (Exception ex) { AnsiConsole.MarkupLineInterpolated( - $"( ERROR ) Unable to remove watermark! {ex.Message}" + $"[red]( ERROR )[/] Error when removing the watermark! {ex.Message}" ); return null; } @@ -739,12 +825,14 @@ List assetsReplacer } catch (Exception ex) { - AnsiConsole.WriteException(ex); - return; + AnsiConsole.MarkupLineInterpolated( + $"[red]( ERROR )[/] Error when writing changes! {ex.Message}" + ); } finally { - File.Delete(uncompressedBundleFile); + if (File.Exists(uncompressedBundleFile)) + File.Delete(uncompressedBundleFile); } } } diff --git a/USSR.csproj b/USSR.csproj index 07cc566..66c06bc 100644 --- a/USSR.csproj +++ b/USSR.csproj @@ -11,6 +11,7 @@ + From ed65fcf5a0aa9cff7cf5dc38a8c13e3b3d4ffe8c Mon Sep 17 00:00:00 2001 From: Bayu Satiyo Date: Wed, 29 Nov 2023 16:09:21 +0700 Subject: [PATCH 3/5] Update classdata.tpk & README - Replace built-in UnityWebData utility with UnityWebTools.NET library Signed-off-by: Bayu Satiyo --- README.md | 17 +- USSR.cs | 40 ++-- UnityWebData.cs | 146 --------------- UnityWebDataHelper.cs | 246 ------------------------- BrotliUtils.cs => Utils/BrotliUtils.cs | 0 GZipUtils.cs => Utils/GZipUtils.cs | 0 Utility.cs => Utils/Utility.cs | 21 +-- classdata.tpk | Bin 1215442 -> 1225486 bytes 8 files changed, 36 insertions(+), 434 deletions(-) delete mode 100644 UnityWebData.cs delete mode 100644 UnityWebDataHelper.cs rename BrotliUtils.cs => Utils/BrotliUtils.cs (100%) rename GZipUtils.cs => Utils/GZipUtils.cs (100%) rename Utility.cs => Utils/Utility.cs (91%) 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 068404a..7610dae 100644 --- a/USSR.cs +++ b/USSR.cs @@ -1,6 +1,7 @@ using System.Reflection; using AssetsTools.NET; using AssetsTools.NET.Extra; +using Kiraio.UnityWebTools; using NativeFileDialogSharp; using Spectre.Console; using USSR.Utilities; @@ -209,6 +210,7 @@ static void Main(string[] args) AnsiConsole.MarkupLine("( INFO ) [green]UnityWebData[/] file selected."); isWebGL = true; webDataFile = selectedFile; + webGLCompressionType = WebGLCompressionType.None; } else if ( Utility.ValidateFile(selectedFile, unityBrotliMagic) @@ -253,7 +255,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); @@ -348,38 +350,36 @@ static void Main(string[] args) { if (isWebGL && assetsReplacer != null) { - if (webGLCompressionType != WebGLCompressionType.None) - UnityWebDataHelper.PackFilesToWebData( - unpackedWebDataDirectory, - webDataFile - ); - switch (webGLCompressionType) { 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); + + if (File.Exists(webDataFile)) + File.Delete(webDataFile); break; 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); + + if (File.Exists(webDataFile)) + File.Delete(webDataFile); break; case WebGLCompressionType.None: default: - UnityWebDataHelper.PackFilesToWebData( - unpackedWebDataDirectory, - selectedFile - ); + UnityWebTool.Pack(unpackedWebDataDirectory, selectedFile); break; } - - if (webGLCompressionType != WebGLCompressionType.None) - File.Delete(webDataFile); } } catch (Exception ex) @@ -677,13 +677,11 @@ out splashScreenIndex buildSettingsBase["hasPROVersion"].AsBool = !hasProVersion; // true playerSettingsBase["m_ShowUnitySplashLogo"].AsBool = !showUnityLogo; // false - if (totalSplashScreen > 0) - { - AnsiConsole.MarkupLineInterpolated( - $"( INFO ) [green]Removed splash screen at index {splashScreenIndex}.[/]" - ); - splashScreenLogos?.Children.RemoveAt(splashScreenIndex); - } + AnsiConsole.MarkupLineInterpolated( + $"( INFO ) [green]Removed splash screen at index {splashScreenIndex}.[/]" + ); + + splashScreenLogos?.Children.RemoveAt(splashScreenIndex); return new() { 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 100% rename from BrotliUtils.cs rename to Utils/BrotliUtils.cs diff --git a/GZipUtils.cs b/Utils/GZipUtils.cs similarity index 100% rename from GZipUtils.cs rename to Utils/GZipUtils.cs diff --git a/Utility.cs b/Utils/Utility.cs similarity index 91% rename from Utility.cs rename to Utils/Utility.cs index ea42315..4c20635 100644 --- a/Utility.cs +++ b/Utils/Utility.cs @@ -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 8bb82f5cb634cace4ee91d53aa4c8e571e4f6579..fa9f9dec389debac93cfce9769b9b1183e2b9754 100644 GIT binary patch delta 42097 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 delta 39478 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 From a1a37f56864ccf3d329526c66cfe5887498c533a Mon Sep 17 00:00:00 2001 From: Bayu Satiyo Date: Tue, 23 Jan 2024 20:30:53 +0700 Subject: [PATCH 4/5] Workaround for detecting selected file We come back using traditional file name to validate the required file. We use it because Unity asset file header are sometimes changing for every version. If anyone knows better way to do this, please make a PR! Signed-off-by: Bayu Satiyo --- USSR.cs | 339 ++++++++++++++++++++----------------------- Utils/BrotliUtils.cs | 54 ++++--- Utils/GZipUtils.cs | 18 ++- Utils/Utility.cs | 2 +- classdata.tpk | Bin 1225486 -> 1254671 bytes 5 files changed, 207 insertions(+), 206 deletions(-) diff --git a/USSR.cs b/USSR.cs index 7610dae..09767d1 100644 --- a/USSR.cs +++ b/USSR.cs @@ -1,4 +1,4 @@ -using System.Reflection; +using System; using AssetsTools.NET; using AssetsTools.NET.Extra; using Kiraio.UnityWebTools; @@ -12,113 +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 }; - - // Ref: https://github.com/google/brotli/issues/867#issue-739852869 - // I have already test this magic bytes into an asset file, but - // it's turning out that it's not work. - 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 { @@ -126,8 +19,6 @@ enum AssetTypes Bundle } - static AssetTypes assetType; - enum WebGLCompressionType { None, @@ -135,6 +26,7 @@ enum WebGLCompressionType GZip } + static AssetTypes assetType; static WebGLCompressionType webGLCompressionType; static void Main(string[] args) @@ -143,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; @@ -164,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: @@ -192,54 +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 = WebGLCompressionType.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 (selectedFileName.EndsWith("data.br")) + { + isWebGL = true; + webGLCompressionType = WebGLCompressionType.Brotli; + DecompressCompressedWebData(webGLCompressionType, selectedFile, webDataFile); } - else if (Utility.ValidateFile(selectedFile, gzipMagic)) + else if (selectedFileName.EndsWith("data.gz")) { - AnsiConsole.MarkupLine("( INFO ) [green]UnityWebData GZip[/] file selected."); isWebGL = true; webGLCompressionType = WebGLCompressionType.GZip; - - AnsiConsole.MarkupLineInterpolated( - $"( INFO ) Decompressing [green]{selectedFile}[/]..." - ); - GZipUtils.DecompressFile(selectedFile, webDataFile); + 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; } @@ -263,7 +184,7 @@ 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; } @@ -305,7 +226,7 @@ static void Main(string[] args) catch (Exception ex) { AnsiConsole.MarkupLineInterpolated( - $"[red]( ERROR )[/] Error when loading asset class types database! {ex.Message}" + $"[red]( RAWR )[/] Error when loading asset class types database! {ex.Message}" ); goto Cleanup; } @@ -350,6 +271,9 @@ static void Main(string[] args) { if (isWebGL && assetsReplacer != null) { + AnsiConsole.MarkupLineInterpolated( + $"( INFO ) Packing [green]{unpackedWebDataDirectory}[/]..." + ); switch (webGLCompressionType) { case WebGLCompressionType.Brotli: @@ -359,10 +283,6 @@ static void Main(string[] args) $"( INFO ) Compressing [green]{webDataFile}[/] using Brotli compression. Please be patient, it might take some time..." ); BrotliUtils.CompressFile(webDataFile, selectedFile); - // BrotliUtils.WriteUnityIdentifier(selectedFile, unityBrotliMagic); - - if (File.Exists(webDataFile)) - File.Delete(webDataFile); break; case WebGLCompressionType.GZip: UnityWebTool.Pack(unpackedWebDataDirectory, webDataFile); @@ -371,9 +291,6 @@ static void Main(string[] args) $"( INFO ) Compressing [green]{webDataFile}[/] using GZip compression. Please be patient, it might take some time..." ); GZipUtils.CompressFile(webDataFile, selectedFile); - - if (File.Exists(webDataFile)) - File.Delete(webDataFile); break; case WebGLCompressionType.None: default: @@ -385,13 +302,25 @@ static void Main(string[] args) catch (Exception ex) { AnsiConsole.MarkupLineInterpolated( - $"[red]( ERROR )[/] Error when compressing Unity Web Data! {ex.Message}" + $"[red]( RAWR )[/] Error when compressing Unity Web Data! {ex.Message}" ); } finally { - if (Directory.Exists(unpackedWebDataDirectory)) - Directory.Delete(unpackedWebDataDirectory, true); + 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(); @@ -421,7 +350,7 @@ static void PrintHelp() "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]globalgamemanagers[/] | [green]data.unity3d[/] | [green].data[/] | [green].data.br[/] | [green].data.gz[/] | [green].data.unityweb[/]" ); } @@ -439,13 +368,13 @@ static void LoadClassPackage(AssetsManager assetsManager, string tpkFile) catch (Exception ex) { AnsiConsole.MarkupLine( - $"[red]( ERROR )[/] Error when loading class types package! {ex.Message}" + $"[red]( RAWR )[/] Error when loading class types package! {ex.Message}" ); } } else AnsiConsole.MarkupLineInterpolated( - $"[red]( ERROR )[/] TPK file not found: [red]{tpkFile}[/]..." + $"[red]( RAWR )[/] TPK file not found: [red]{tpkFile}[/]..." ); } @@ -473,14 +402,14 @@ AssetsManager assetsManager catch (Exception ex) { AnsiConsole.MarkupLineInterpolated( - $"[red]( ERROR )[/] Error when loading asset file! {ex.Message}" + $"[red]( RAWR )[/] Error when loading asset file! {ex.Message}" ); } } else { AnsiConsole.MarkupLineInterpolated( - $"[red]( ERROR )[/] Asset file not found: [red]{assetFile}[/]" + $"[red]( RAWR )[/] Asset file not found: [red]{assetFile}[/]" ); } @@ -518,14 +447,14 @@ AssetsManager assetsManager catch (Exception ex) { AnsiConsole.MarkupLineInterpolated( - $"[red]( ERROR )[/] Error when loading asset file! {ex.Message}" + $"[red]( RAWR )[/] Error when loading asset file! {ex.Message}" ); } } else { AnsiConsole.MarkupLineInterpolated( - $"[red]( ERROR )[/] Asset file not found: [red]{assetFile}[/]" + $"[red]( RAWR )[/] Asset file not found: [red]{assetFile}[/]" ); } @@ -558,14 +487,14 @@ AssetsManager assetsManager catch (Exception ex) { AnsiConsole.MarkupLineInterpolated( - $"[red]( ERROR )[/] Error when loading bundle file! {ex.Message}" + $"[red]( RAWR )[/] Error when loading bundle file! {ex.Message}" ); } } else { AnsiConsole.MarkupLineInterpolated( - $"[red]( ERROR )[/] Bundle file not found: [red]{bundleFile}[/]" + $"[red]( RAWR )[/] Bundle file not found: [red]{bundleFile}[/]" ); } @@ -602,10 +531,10 @@ AssetsManager assetsManager if (playerSettingsBase == null) { AnsiConsole.MarkupLine( - "[red]( ERROR )[/] Can\'t get Player Settings fields! It\'s possible that this current version of Unity are currently not supported yet." + "[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[/] from there: [link green]https://nightly.link/AssetRipper/Tpk/workflows/type_tree_tpk/master/uncompressed_file.zip[/] and try again." + "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." @@ -621,7 +550,7 @@ AssetsManager assetsManager if (hasProVersion && !showUnityLogo) { AnsiConsole.MarkupLine( - "[yellow]( WARN ) Unity splash screen have been removed![/]" + "[yellow]( WARN ) Unity splash screen already removed![/]" ); return null; } @@ -640,7 +569,7 @@ AssetsManager assetsManager { case 0: AnsiConsole.MarkupLine( - "[yellow]Did you set the splash screen [bold]Draw Mode[/] to [bold]Unity Logo Below[/]? That\'s useless..[/]" + "[yellow]( WARN ) Nothing to do. Finally, taking a rest :)[/]" ); return null; case 1: @@ -649,7 +578,7 @@ AssetsManager assetsManager } AnsiConsole.MarkupLine( - "What order are Unity splash screen logo in your Player Settings? (Start from 0)" + "What order are Unity splash screen logo in your Player Settings? (Start from 0 [upmost])" ); InputLogoIndex: @@ -663,14 +592,14 @@ out splashScreenIndex if (splashScreenIndex < 0 && splashScreenIndex >= totalSplashScreen) { AnsiConsole.MarkupLineInterpolated( - $"[red]( ERROR )[/] There's no splash screen at index [red]{splashScreenIndex}[/]! Try again." + $"[red]( RAWR )[/] There's no splash screen at index [red]{splashScreenIndex}[/]! Try again." ); goto InputLogoIndex; } RemoveSplashScreen: AnsiConsole.MarkupLineInterpolated( - $"( INFO ) Set [green]hasProVersion = {!hasProVersion}[/] | [green]m_ShowUnitySplashLogo = {!showUnityLogo}[/]" + $"( INFO ) Set [green]hasProVersion[/] = [green]{!hasProVersion}[/] | [green]m_ShowUnitySplashLogo[/] = [green]{!showUnityLogo}[/]" ); // Remove Unity splash screen by flipping these boolean fields @@ -678,7 +607,7 @@ out splashScreenIndex playerSettingsBase["m_ShowUnitySplashLogo"].AsBool = !showUnityLogo; // false AnsiConsole.MarkupLineInterpolated( - $"( INFO ) [green]Removed splash screen at index {splashScreenIndex}.[/]" + $"( INFO ) [green]Splash screen removed at index {splashScreenIndex}.[/]" ); splashScreenLogos?.Children.RemoveAt(splashScreenIndex); @@ -700,7 +629,7 @@ out splashScreenIndex catch (Exception ex) { AnsiConsole.MarkupLineInterpolated( - $"[red]( ERROR )[/] Error when removing the splash screen! {ex.Message}" + $"[red]( RAWR )[/] Error when removing the splash screen! {ex.Message}" ); return null; } @@ -752,7 +681,7 @@ out splashScreenIndex catch (Exception ex) { AnsiConsole.MarkupLineInterpolated( - $"[red]( ERROR )[/] Error when removing the watermark! {ex.Message}" + $"[red]( RAWR )[/] Error when removing the watermark! {ex.Message}" ); return null; } @@ -824,7 +753,7 @@ List assetsReplacer catch (Exception ex) { AnsiConsole.MarkupLineInterpolated( - $"[red]( ERROR )[/] Error when writing changes! {ex.Message}" + $"[red]( RAWR )[/] Error when writing changes! {ex.Message}" ); } finally @@ -833,5 +762,57 @@ List assetsReplacer 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/Utils/BrotliUtils.cs b/Utils/BrotliUtils.cs index aceb716..abed1ac 100644 --- a/Utils/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/Utils/GZipUtils.cs b/Utils/GZipUtils.cs index 02df605..7d28d53 100644 --- a/Utils/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/Utils/Utility.cs b/Utils/Utility.cs index 4c20635..cc478cd 100644 --- a/Utils/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. /// /// /// diff --git a/classdata.tpk b/classdata.tpk index fa9f9dec389debac93cfce9769b9b1183e2b9754..42a59112a8e9d47e7c7a75622dbd3112fca44b57 100644 GIT binary patch delta 57045 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 P*-!L4h-?vmbq#nRQ`w0&6Q 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 Date: Tue, 23 Jan 2024 20:42:10 +0700 Subject: [PATCH 5/5] Add .NET 8 in build.yml action Signed-off-by: Bayu Satiyo --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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: