using AssetStudio; using AssetStudioCLI.Options; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Linq; using static AssetStudioCLI.Exporter; using Ansi = AssetStudioCLI.CLIAnsiColors; namespace AssetStudioCLI { internal class Studio { public AssetsManager assetsManager = new AssetsManager(); public List parsedAssetsList = new List(); private readonly CLIOptions options; public Studio(CLIOptions cliOptions) { Progress.Default = new Progress(ShowCurProgressValue); options = cliOptions; } private void ShowCurProgressValue(int value) { Console.Write($"[{value:000}%]\r"); } public bool LoadAssets() { var isLoaded = false; assetsManager.SpecifyUnityVersion = options.o_unityVersion.Value; assetsManager.SetAssetFilter(options.o_exportAssetTypes.Value); if (Directory.Exists(options.inputPath)) { assetsManager.LoadFolder(options.inputPath); } else { assetsManager.LoadFiles(options.inputPath); } if (assetsManager.assetsFileList.Count == 0) { Logger.Warning("No Unity file can be loaded."); } else { isLoaded = true; } return isLoaded; } public void ParseAssets() { Logger.Info("Parse assets..."); var fileAssetsList = new List(); var containers = new Dictionary(); var objectCount = assetsManager.assetsFileList.Sum(x => x.Objects.Count); Progress.Reset(); var i = 0; foreach (var assetsFile in assetsManager.assetsFileList) { foreach (var asset in assetsFile.Objects) { var assetItem = new AssetItem(asset); assetItem.UniqueID = "_#" + i; var isExportable = false; switch (asset) { case AssetBundle m_AssetBundle: foreach (var m_Container in m_AssetBundle.m_Container) { var preloadIndex = m_Container.Value.preloadIndex; var preloadSize = m_Container.Value.preloadSize; var preloadEnd = preloadIndex + preloadSize; for (int k = preloadIndex; k < preloadEnd; k++) { var pptr = m_AssetBundle.m_PreloadTable[k]; if (pptr.TryGet(out var obj)) { containers[obj] = m_Container.Key; } } } break; case ResourceManager m_ResourceManager: foreach (var m_Container in m_ResourceManager.m_Container) { if (m_Container.Value.TryGet(out var obj)) { containers[obj] = m_Container.Key; } } break; case Texture2D m_Texture2D: if (!string.IsNullOrEmpty(m_Texture2D.m_StreamData?.path)) assetItem.FullSize = asset.byteSize + m_Texture2D.m_StreamData.size; assetItem.Text = m_Texture2D.m_Name; break; case AudioClip m_AudioClip: if (!string.IsNullOrEmpty(m_AudioClip.m_Source)) assetItem.FullSize = asset.byteSize + m_AudioClip.m_Size; assetItem.Text = m_AudioClip.m_Name; break; case VideoClip m_VideoClip: if (!string.IsNullOrEmpty(m_VideoClip.m_OriginalPath)) assetItem.FullSize = asset.byteSize + m_VideoClip.m_ExternalResources.m_Size; assetItem.Text = m_VideoClip.m_Name; break; case MovieTexture _: case TextAsset _: case Font _: case Sprite _: assetItem.Text = ((NamedObject)asset).m_Name; break; case Shader m_Shader: assetItem.Text = m_Shader.m_ParsedForm?.m_Name ?? m_Shader.m_Name; break; case MonoBehaviour m_MonoBehaviour: if (m_MonoBehaviour.m_Name == "" && m_MonoBehaviour.m_Script.TryGet(out var m_Script)) { assetItem.Text = m_Script.m_ClassName; } else { assetItem.Text = m_MonoBehaviour.m_Name; } break; } if (assetItem.Text == "") { assetItem.Text = assetItem.TypeString + assetItem.UniqueID; } isExportable = options.o_exportAssetTypes.Value.Contains(asset.type); if (isExportable) { fileAssetsList.Add(assetItem); } Progress.Report(++i, objectCount); } foreach (var asset in fileAssetsList) { if (containers.ContainsKey(asset.Asset)) { asset.Container = containers[asset.Asset]; } } parsedAssetsList.AddRange(fileAssetsList); containers.Clear(); fileAssetsList.Clear(); } } public void ShowExportableAssetsInfo() { var exportableAssetsCountDict = new Dictionary(); if (parsedAssetsList.Count > 0) { foreach (var asset in parsedAssetsList) { if (exportableAssetsCountDict.ContainsKey(asset.Type)) { exportableAssetsCountDict[asset.Type] += 1; } else { exportableAssetsCountDict.Add(asset.Type, 1); } } var info = "\n[Exportable Assets Count]\n"; foreach (var assetType in exportableAssetsCountDict.Keys) { info += $"# {assetType}: {exportableAssetsCountDict[assetType]}\n"; } if (exportableAssetsCountDict.Count > 1) { info += $"#\n# Total: {parsedAssetsList.Count} assets"; } if (options.o_logLevel.Value > LoggerEvent.Info) { Console.WriteLine(info); } else { Logger.Info(info); } } } public void FilterAssets() { var assetsCount = parsedAssetsList.Count; var filteredAssets = new List(); switch(options.filterBy) { case FilterBy.Name: filteredAssets = parsedAssetsList.FindAll(x => options.o_filterByName.Value.Any(y => x.Text.ToString().IndexOf(y, StringComparison.OrdinalIgnoreCase) >= 0)); Logger.Info( $"Found [{filteredAssets.Count}/{assetsCount}] asset(s) " + $"that contain {$"\"{string.Join("\", \"", options.o_filterByName.Value)}\"".Color(Ansi.BrightYellow)} in their Names." ); break; case FilterBy.Container: filteredAssets = parsedAssetsList.FindAll(x => options.o_filterByContainer.Value.Any(y => x.Container.ToString().IndexOf(y, StringComparison.OrdinalIgnoreCase) >= 0)); Logger.Info( $"Found [{filteredAssets.Count}/{assetsCount}] asset(s) " + $"that contain {$"\"{string.Join("\", \"", options.o_filterByContainer.Value)}\"".Color(Ansi.BrightYellow)} in their Containers." ); break; case FilterBy.PathID: filteredAssets = parsedAssetsList.FindAll(x => options.o_filterByPathID.Value.Any(y => x.m_PathID.ToString().IndexOf(y, StringComparison.OrdinalIgnoreCase) >= 0)); Logger.Info( $"Found [{filteredAssets.Count}/{assetsCount}] asset(s) " + $"that contain {$"\"{string.Join("\", \"", options.o_filterByPathID.Value)}\"".Color(Ansi.BrightYellow)} in their PathIDs." ); break; case FilterBy.NameOrContainer: filteredAssets = parsedAssetsList.FindAll(x => options.o_filterByText.Value.Any(y => x.Text.ToString().IndexOf(y, StringComparison.OrdinalIgnoreCase) >= 0) || options.o_filterByText.Value.Any(y => x.Container.ToString().IndexOf(y, StringComparison.OrdinalIgnoreCase) >= 0) ); Logger.Info( $"Found [{filteredAssets.Count}/{assetsCount}] asset(s) " + $"that contain {$"\"{string.Join("\", \"", options.o_filterByText.Value)}\"".Color(Ansi.BrightYellow)} in their Names or Contaniers." ); break; case FilterBy.NameAndContainer: filteredAssets = parsedAssetsList.FindAll(x => options.o_filterByName.Value.Any(y => x.Text.ToString().IndexOf(y, StringComparison.OrdinalIgnoreCase) >= 0) && options.o_filterByContainer.Value.Any(y => x.Container.ToString().IndexOf(y, StringComparison.OrdinalIgnoreCase) >= 0) ); Logger.Info( $"Found [{filteredAssets.Count}/{assetsCount}] asset(s) " + $"that contain {$"\"{string.Join("\", \"", options.o_filterByContainer.Value)}\"".Color(Ansi.BrightYellow)} in their Containers " + $"and {$"\"{string.Join("\", \"", options.o_filterByName.Value)}\"".Color(Ansi.BrightYellow)} in their Names." ); break; } parsedAssetsList.Clear(); parsedAssetsList = filteredAssets; } public void ExportAssets() { var savePath = options.o_outputFolder.Value; var toExportCount = parsedAssetsList.Count; var exportedCount = 0; foreach (var asset in parsedAssetsList) { string exportPath; switch (options.o_groupAssetsBy.Value) { case AssetGroupOption.TypeName: exportPath = Path.Combine(savePath, asset.TypeString); break; case AssetGroupOption.ContainerPath: if (!string.IsNullOrEmpty(asset.Container)) { exportPath = Path.Combine(savePath, Path.GetDirectoryName(asset.Container)); } else { exportPath = savePath; } break; case AssetGroupOption.SourceFileName: if (string.IsNullOrEmpty(asset.SourceFile.originalPath)) { exportPath = Path.Combine(savePath, asset.SourceFile.fileName + "_export"); } else { exportPath = Path.Combine(savePath, Path.GetFileName(asset.SourceFile.originalPath) + "_export", asset.SourceFile.fileName); } break; default: exportPath = savePath; break; } exportPath += Path.DirectorySeparatorChar; try { switch (options.o_workMode.Value) { case WorkMode.ExportRaw: Logger.Debug($"{options.o_workMode}: {asset.Type} : {asset.Container} : {asset.Text}"); if (ExportRawFile(asset, exportPath)) { exportedCount++; } break; case WorkMode.Dump: Logger.Debug($"{options.o_workMode}: {asset.Type} : {asset.Container} : {asset.Text}"); if (ExportDumpFile(asset, exportPath, options)) { exportedCount++; } break; case WorkMode.Export: Logger.Debug($"{options.o_workMode}: {asset.Type} : {asset.Container} : {asset.Text}"); if (ExportConvertFile(asset, exportPath, options)) { exportedCount++; } break; } } catch (Exception ex) { Logger.Error($"{asset.SourceFile.originalPath}: [{$"{asset.Type}: {asset.Text}".Color(Ansi.BrightRed)}] : Export error\n{ex}"); } Console.Write($"Exported [{exportedCount}/{toExportCount}]\r"); } Console.WriteLine(""); if (exportedCount == 0) { Logger.Info("Nothing exported."); } else if (toExportCount > exportedCount) { Logger.Info($"Finished exporting {exportedCount} asset(s) to \"{options.o_outputFolder.Value.Color(Ansi.BrightYellow)}\"."); } else { Logger.Info($"Finished exporting {exportedCount} asset(s) to \"{options.o_outputFolder.Value.Color(Ansi.BrightGreen)}\"."); } if (toExportCount > exportedCount) { Logger.Info($"{toExportCount - exportedCount} asset(s) skipped (not extractable or file(s) already exist)."); } } public void ExportAssetList() { var savePath = options.o_outputFolder.Value; switch (options.o_exportAssetList.Value) { case ExportListType.XML: var filename = Path.Combine(savePath, "assets.xml"); var doc = new XDocument( new XElement("Assets", new XAttribute("filename", filename), new XAttribute("createdAt", DateTime.UtcNow.ToString("s")), parsedAssetsList.Select( asset => new XElement("Asset", new XElement("Name", asset.Text), new XElement("Container", asset.Container), new XElement("Type", new XAttribute("id", (int)asset.Type), asset.TypeString), new XElement("PathID", asset.m_PathID), new XElement("Source", asset.SourceFile.fullName), new XElement("Size", asset.FullSize) ) ) ) ); doc.Save(filename); break; } Logger.Info($"Finished exporting asset list with {parsedAssetsList.Count} items."); } } }