diff --git a/.github/workflows/publish-cli-folderexport.yml b/.github/workflows/publish-cli-folderexport.yml new file mode 100644 index 0000000000..3a6689f5d5 --- /dev/null +++ b/.github/workflows/publish-cli-folderexport.yml @@ -0,0 +1,39 @@ +name: Publish-CLI-FolderExport + +on: + push: + branches: [master] + pull_request: + branches: [master] + workflow_dispatch: + +jobs: + publish: + runs-on: ${{ matrix.config.os }} + + strategy: + matrix: + config: + - { name: win_x64, os: windows-latest, runtime: win-x64, executable: AssetRipper.CLI.FolderExport.exe } + - { name: win_arm64, os: windows-latest, runtime: win-arm64, executable: AssetRipper.CLI.FolderExport.exe } + - { name: linux_x64, os: ubuntu-latest, runtime: linux-x64, executable: AssetRipper.CLI.FolderExport } + - { name: mac_x64, os: macos-latest, runtime: osx-x64, executable: AssetRipper.CLI.FolderExport } + - { name: mac_arm64, os: macos-latest, runtime: osx-arm64, executable: AssetRipper.CLI.FolderExport } + + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + + - name: Publish + run: dotnet publish -c Release -r ${{ matrix.config.runtime }} --self-contained + working-directory: ./Source/AssetRipper.CLI.FolderExport/ + + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: AssetRipper_CLI_FolderExport_${{ matrix.config.name }} + path: ./Source/AssetRipper.CLI.FolderExport/bin/Release/${{ matrix.config.runtime }}/publish/${{ matrix.config.executable }} + if-no-files-found: error diff --git a/AssetRipper.sln b/AssetRipper.sln index 123101600d..2a1e9ebd7a 100644 --- a/AssetRipper.sln +++ b/AssetRipper.sln @@ -160,7 +160,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AssetRipper.GUI.Web", "Sour EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AssetRipper.Export.Modules.Models", "Source\AssetRipper.Export.Modules.Models\AssetRipper.Export.Modules.Models.csproj", "{B827502A-EA76-4C16-B246-0E625A7ED80E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AssetRipper.GUI.Web.Tests", "Source\AssetRipper.GUI.Web.Tests\AssetRipper.GUI.Web.Tests.csproj", "{9EDD3621-6E2B-41AD-8F6A-F8C4CD113566}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AssetRipper.GUI.Web.Tests", "Source\AssetRipper.GUI.Web.Tests\AssetRipper.GUI.Web.Tests.csproj", "{9EDD3621-6E2B-41AD-8F6A-F8C4CD113566}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AssetRipper.CLI.FolderExport", "Source\AssetRipper.CLI.FolderExport\AssetRipper.CLI.FolderExport.csproj", "{445709B2-09C3-47E9-B48D-15FE423489F2}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -344,6 +346,10 @@ Global {9EDD3621-6E2B-41AD-8F6A-F8C4CD113566}.Debug|Any CPU.Build.0 = Debug|Any CPU {9EDD3621-6E2B-41AD-8F6A-F8C4CD113566}.Release|Any CPU.ActiveCfg = Release|Any CPU {9EDD3621-6E2B-41AD-8F6A-F8C4CD113566}.Release|Any CPU.Build.0 = Release|Any CPU + {445709B2-09C3-47E9-B48D-15FE423489F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {445709B2-09C3-47E9-B48D-15FE423489F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {445709B2-09C3-47E9-B48D-15FE423489F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {445709B2-09C3-47E9-B48D-15FE423489F2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Source/AssetRipper.Assets/Bundles/GameBundle.FromPaths.cs b/Source/AssetRipper.Assets/Bundles/GameBundle.FromPaths.cs index 2c1eef8451..2345d28ec1 100644 --- a/Source/AssetRipper.Assets/Bundles/GameBundle.FromPaths.cs +++ b/Source/AssetRipper.Assets/Bundles/GameBundle.FromPaths.cs @@ -24,19 +24,25 @@ public static GameBundle FromPaths(IEnumerable paths, AssetFactoryBase a initializer?.OnCreated(gameBundle, assetFactory); gameBundle.InitializeFromPaths(paths, assetFactory, initializer); initializer?.OnPathsLoaded(gameBundle, assetFactory); + Console.WriteLine("Initializing dependency lists..."); gameBundle.InitializeAllDependencyLists(initializer?.DependencyProvider); initializer?.OnDependenciesInitialized(gameBundle, assetFactory); return gameBundle; } - + private void InitializeFromPaths(IEnumerable paths, AssetFactoryBase assetFactory, IGameInitializer? initializer) { ResourceProvider = initializer?.ResourceProvider; List fileStack = LoadFilesAndDependencies(paths, initializer?.DependencyProvider); + int totalFiles = fileStack.Count; + int processedFiles = 0; UnityVersion defaultVersion = initializer is null ? default : initializer.DefaultVersion; while (fileStack.Count > 0) { + if(processedFiles % 100 == 0) + Console.WriteLine($"Bundling file... {processedFiles}/{totalFiles} ({(processedFiles * 100 / totalFiles)}%)"); + switch (RemoveLastItem(fileStack)) { case SerializedFile serializedFile: @@ -53,7 +59,11 @@ private void InitializeFromPaths(IEnumerable paths, AssetFactoryBase ass AddFailed(failedFile); break; } + + processedFiles++; } + + Console.WriteLine($"Bundling file... {processedFiles}/{totalFiles} [100%]"); } private static FileBase RemoveLastItem(List list) diff --git a/Source/AssetRipper.CLI.FolderExport/AssetRipper.CLI.FolderExport.csproj b/Source/AssetRipper.CLI.FolderExport/AssetRipper.CLI.FolderExport.csproj new file mode 100644 index 0000000000..634e6f3010 --- /dev/null +++ b/Source/AssetRipper.CLI.FolderExport/AssetRipper.CLI.FolderExport.csproj @@ -0,0 +1,18 @@ + + + + Exe + enable + enable + true + + + + + + + + + + + diff --git a/Source/AssetRipper.CLI.FolderExport/Program.cs b/Source/AssetRipper.CLI.FolderExport/Program.cs new file mode 100644 index 0000000000..6fa57b7850 --- /dev/null +++ b/Source/AssetRipper.CLI.FolderExport/Program.cs @@ -0,0 +1,106 @@ +using AssetRipper.Export.UnityProjects; +using AssetRipper.Export.UnityProjects.Configuration; +using AssetRipper.Import.Configuration; +using AssetRipper.Primitives; +using AssetRipper.Processing; +using AssetRipper.Processing.Configuration; +using System.CommandLine; +using System.Diagnostics; + +public class Program +{ + private static readonly string DefaultUnityVersion = "2022.3.22f1"; + + public static void Main(string[] args) + { + Console.WriteLine(Launch(args) ? "Files exported successfully." : "There was an error exporting the files."); + } + + public static bool Launch(string[] args) + { + RootCommand rootCommand = []; + + Option folderOption = new( + "--folder", + "Path to the input folder") + { + IsRequired = true + }; + rootCommand.AddOption(folderOption); + + Option outputOption = new( + "--output", + "Path to the output folder") + { + IsRequired = true + }; + rootCommand.AddOption(outputOption); + + Option unityVersionOption = new( + "--unity-version", + () => DefaultUnityVersion, + $"Unity version to use (default is {DefaultUnityVersion})"); + rootCommand.AddOption(unityVersionOption); + + bool result = false; + + rootCommand.SetHandler((string folderPath, string outputPath, string unityVersion) => + { + Stopwatch stopwatch = new(); + stopwatch.Start(); + DirectoryInfo folder = new(folderPath); + DirectoryInfo output = new(outputPath); + + if (!folder.Exists) + { + Console.WriteLine($"The specified input folder '{folder.FullName}' does not exist."); + return; + } + + if (!output.Exists) + { + Console.WriteLine($"The specified output folder '{output.FullName}' does not exist."); + return; + } + + Console.WriteLine($"Input folder: {folder.FullName}"); + Console.WriteLine($"Output folder: {output.FullName}"); + Console.WriteLine($"Unity version: {unityVersion}"); + Console.WriteLine("Initializing..."); + + LibraryConfiguration settings = new(); + settings.LoadFromDefaultPath(); + ExportHandler exportHandler = new(LoadSettings(unityVersion)); + + GameData gameData = exportHandler.LoadAndProcess(new string[] { folder.FullName }); + + exportHandler.Export(gameData, output.FullName); + + stopwatch.Stop(); + TimeSpan ts = stopwatch.Elapsed; + + string elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}", + ts.Hours, ts.Minutes, ts.Seconds, + ts.Milliseconds / 10); + + Console.WriteLine($"Process finished in {elapsedTime}"); + + result = true; + + }, folderOption, outputOption, unityVersionOption); + + rootCommand.Invoke(args); + + return result; + } + + private static LibraryConfiguration LoadSettings(string unityVersion) + { + LibraryConfiguration settings = new(); + settings.LoadFromDefaultPath(); + settings.ProcessingSettings.BundledAssetsExportMode = BundledAssetsExportMode.GroupByBundleName; + settings.ImportSettings.DefaultVersion = UnityVersion.Parse(unityVersion); + settings.ImportSettings.TargetVersion = UnityVersion.Parse(unityVersion); + return settings; + } +} diff --git a/Source/AssetRipper.Export.UnityProjects/ExportHandler.cs b/Source/AssetRipper.Export.UnityProjects/ExportHandler.cs index 50aa1869aa..a438f6ea73 100644 --- a/Source/AssetRipper.Export.UnityProjects/ExportHandler.cs +++ b/Source/AssetRipper.Export.UnityProjects/ExportHandler.cs @@ -40,6 +40,7 @@ public GameData Load(IReadOnlyList paths) } GameStructure gameStructure = GameStructure.Load(paths, Settings); + Console.WriteLine("Finished Loading game structure"); GameData gameData = GameData.FromGameStructure(gameStructure); Logger.Info(LogCategory.Import, "Finished reading files"); return gameData; @@ -78,6 +79,7 @@ protected virtual IEnumerable GetProcessors() public void Export(GameData gameData, string outputPath) { + Console.WriteLine("Starting export..."); Logger.Info(LogCategory.Export, "Starting export"); Logger.Info(LogCategory.Export, $"Attempting to export assets to {outputPath}..."); Logger.Info(LogCategory.Export, $"Game files have these Unity versions:{GetListOfVersions(gameData.GameBundle)}"); @@ -124,7 +126,9 @@ protected virtual IEnumerable GetPostExporters() public GameData LoadAndProcess(IReadOnlyList paths) { + Console.WriteLine($"Loading game data from paths [{string.Join(", ", paths)}]..."); GameData gameData = Load(paths); + Console.WriteLine("Processing game data..."); Process(gameData); return gameData; } diff --git a/Source/AssetRipper.Export.UnityProjects/ProjectExporter.cs b/Source/AssetRipper.Export.UnityProjects/ProjectExporter.cs index 0d19eaa8b7..16579b870f 100644 --- a/Source/AssetRipper.Export.UnityProjects/ProjectExporter.cs +++ b/Source/AssetRipper.Export.UnityProjects/ProjectExporter.cs @@ -86,6 +86,8 @@ public void Export(GameBundle fileCollection, CoreConfiguration options) EventExportStarted?.Invoke(); ProjectAssetContainer container = new ProjectAssetContainer(this, options, fileCollection.FetchAssets(), collections); + int totalExportable = collections.FindAll(col => col.Exportable).Count(); + int exportedCount = 0; for (int i = 0; i < collections.Count; i++) { IExportCollection collection = collections[i]; @@ -93,6 +95,9 @@ public void Export(GameBundle fileCollection, CoreConfiguration options) if (collection.Exportable) { Logger.Info(LogCategory.ExportProgress, $"Exporting '{collection.Name}'"); + if(exportedCount % 100 == 0) + Console.WriteLine($"Exporting... {exportedCount}/{totalExportable} ({exportedCount * 100 / totalExportable}%)"); + exportedCount++; bool exportedSuccessfully = collection.Export(container, options.ProjectRootPath); if (!exportedSuccessfully) { @@ -101,6 +106,7 @@ public void Export(GameBundle fileCollection, CoreConfiguration options) } EventExportProgressUpdated?.Invoke(i, collections.Count); } + Console.WriteLine($"Exporting... {totalExportable}/{totalExportable} (100%)"); EventExportFinished?.Invoke(); }