using AssetStudio; using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Windows.Forms; using static AssetStudioGUI.Exporter; using Object = AssetStudio.Object; namespace AssetStudioGUI { internal enum ExportType { Convert, Raw, Dump } internal static class Studio { public static AssetsManager assetsManager = new AssetsManager(); public static AssemblyLoader assemblyLoader = new AssemblyLoader(); public static List exportableAssets = new List(); public static List visibleAssets = new List(); internal static Action StatusStripUpdate = x => { }; public static int ExtractFolder(string path, string savePath) { int extractedCount = 0; Progress.Reset(); var files = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories); for (int i = 0; i < files.Length; i++) { var file = files[i]; var fileOriPath = Path.GetDirectoryName(file); var fileSavePath = fileOriPath.Replace(path, savePath); extractedCount += ExtractFile(file, fileSavePath); Progress.Report(i + 1, files.Length); } return extractedCount; } public static int ExtractFile(string[] fileNames, string savePath) { int extractedCount = 0; Progress.Reset(); for (var i = 0; i < fileNames.Length; i++) { var fileName = fileNames[i]; extractedCount += ExtractFile(fileName, savePath); Progress.Report(i + 1, fileNames.Length); } return extractedCount; } public static int ExtractFile(string fileName, string savePath) { int extractedCount = 0; var type = ImportHelper.CheckFileType(fileName, out var reader); if (type == FileType.BundleFile) extractedCount += ExtractBundleFile(fileName, reader, savePath); else if (type == FileType.WebFile) extractedCount += ExtractWebDataFile(fileName, reader, savePath); else reader.Dispose(); return extractedCount; } private static int ExtractBundleFile(string bundleFilePath, EndianBinaryReader reader, string savePath) { StatusStripUpdate($"Decompressing {Path.GetFileName(bundleFilePath)} ..."); var bundleFile = new BundleFile(reader, bundleFilePath); reader.Dispose(); if (bundleFile.fileList.Length > 0) { var extractPath = Path.Combine(savePath, Path.GetFileName(bundleFilePath) + "_unpacked"); return ExtractStreamFile(extractPath, bundleFile.fileList); } return 0; } private static int ExtractWebDataFile(string webFilePath, EndianBinaryReader reader, string savePath) { StatusStripUpdate($"Decompressing {Path.GetFileName(webFilePath)} ..."); var webFile = new WebFile(reader); reader.Dispose(); if (webFile.fileList.Length > 0) { var extractPath = Path.Combine(savePath, Path.GetFileName(webFilePath) + "_unpacked"); return ExtractStreamFile(extractPath, webFile.fileList); } return 0; } private static int ExtractStreamFile(string extractPath, StreamFile[] fileList) { int extractedCount = 0; foreach (var file in fileList) { var filePath = Path.Combine(extractPath, file.fileName); if (!Directory.Exists(extractPath)) { Directory.CreateDirectory(extractPath); } if (!File.Exists(filePath)) { using (var fileStream = File.Create(filePath)) { file.stream.CopyTo(fileStream); } extractedCount += 1; } file.stream.Dispose(); } return extractedCount; } public static (string, List) BuildAssetData() { StatusStripUpdate("Building asset list..."); string productName = null; var objectCount = assetsManager.assetsFileList.Sum(x => x.Objects.Count); var objectAssetItemDic = new Dictionary(objectCount); var containers = new List<(PPtr, string)>(); int i = 0; Progress.Reset(); foreach (var assetsFile in assetsManager.assetsFileList) { foreach (var asset in assetsFile.Objects) { var assetItem = new AssetItem(asset); objectAssetItemDic.Add(asset, assetItem); assetItem.UniqueID = " #" + i; var exportable = false; switch (asset) { case GameObject m_GameObject: assetItem.Text = m_GameObject.m_Name; 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; exportable = true; 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; exportable = true; break; case VideoClip m_VideoClip: if (!string.IsNullOrEmpty(m_VideoClip.m_OriginalPath)) assetItem.FullSize = asset.byteSize + (long)m_VideoClip.m_ExternalResources.m_Size; assetItem.Text = m_VideoClip.m_Name; exportable = true; break; case Shader m_Shader: assetItem.Text = m_Shader.m_ParsedForm?.m_Name ?? m_Shader.m_Name; exportable = true; break; case Mesh _: case TextAsset _: case AnimationClip _: case Font _: case MovieTexture _: case Sprite _: assetItem.Text = ((NamedObject)asset).m_Name; exportable = true; break; case Animator m_Animator: if (m_Animator.m_GameObject.TryGet(out var gameObject)) { assetItem.Text = gameObject.m_Name; } exportable = true; 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; } exportable = true; break; case PlayerSettings m_PlayerSettings: productName = m_PlayerSettings.productName; break; 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++) { containers.Add((m_AssetBundle.m_PreloadTable[k], m_Container.Key)); } } assetItem.Text = m_AssetBundle.m_Name; break; case ResourceManager m_ResourceManager: foreach (var m_Container in m_ResourceManager.m_Container) { containers.Add((m_Container.Value, m_Container.Key)); } break; case NamedObject m_NamedObject: assetItem.Text = m_NamedObject.m_Name; break; } if (assetItem.Text == "") { assetItem.Text = assetItem.TypeString + assetItem.UniqueID; } if (Properties.Settings.Default.displayAll || exportable) { exportableAssets.Add(assetItem); } Progress.Report(++i, objectCount); } } foreach ((var pptr, var container) in containers) { if (pptr.TryGet(out var obj)) { objectAssetItemDic[obj].Container = container; } } foreach (var tmp in exportableAssets) { tmp.SetSubItems(); } containers.Clear(); visibleAssets = exportableAssets; StatusStripUpdate("Building tree structure..."); var treeNodeCollection = new List(); var treeNodeDictionary = new Dictionary(); var assetsFileCount = assetsManager.assetsFileList.Count; int j = 0; Progress.Reset(); foreach (var assetsFile in assetsManager.assetsFileList) { var fileNode = new GameObjectTreeNode(assetsFile.fileName); //RootNode foreach (var obj in assetsFile.Objects) { if (obj is GameObject m_GameObject) { if (!treeNodeDictionary.TryGetValue(m_GameObject, out var currentNode)) { currentNode = new GameObjectTreeNode(m_GameObject); treeNodeDictionary.Add(m_GameObject, currentNode); } foreach (var pptr in m_GameObject.m_Components) { if (pptr.TryGet(out var m_Component)) { objectAssetItemDic[m_Component].TreeNode = currentNode; if (m_Component is MeshFilter m_MeshFilter) { if (m_MeshFilter.m_Mesh.TryGet(out var m_Mesh)) { objectAssetItemDic[m_Mesh].TreeNode = currentNode; } } else if (m_Component is SkinnedMeshRenderer m_SkinnedMeshRenderer) { if (m_SkinnedMeshRenderer.m_Mesh.TryGet(out var m_Mesh)) { objectAssetItemDic[m_Mesh].TreeNode = currentNode; } } } } var parentNode = fileNode; if (m_GameObject.m_Transform != null) { if (m_GameObject.m_Transform.m_Father.TryGet(out var m_Father)) { if (m_Father.m_GameObject.TryGet(out var parentGameObject)) { if (!treeNodeDictionary.TryGetValue(parentGameObject, out parentNode)) { parentNode = new GameObjectTreeNode(parentGameObject); treeNodeDictionary.Add(parentGameObject, parentNode); } } } } parentNode.Nodes.Add(currentNode); } } if (fileNode.Nodes.Count > 0) { treeNodeCollection.Add(fileNode); } Progress.Report(++j, assetsFileCount); } treeNodeDictionary.Clear(); objectAssetItemDic.Clear(); return (productName, treeNodeCollection); } public static Dictionary> BuildClassStructure() { var typeMap = new Dictionary>(); foreach (var assetsFile in assetsManager.assetsFileList) { if (typeMap.TryGetValue(assetsFile.unityVersion, out var curVer)) { foreach (var type in assetsFile.m_Types.Where(x => x.m_Nodes != null)) { var key = type.classID; if (type.m_ScriptTypeIndex >= 0) { key = -1 - type.m_ScriptTypeIndex; } curVer[key] = new TypeTreeItem(key, type.m_Nodes); } } else { var items = new SortedDictionary(); foreach (var type in assetsFile.m_Types.Where(x => x.m_Nodes != null)) { var key = type.classID; if (type.m_ScriptTypeIndex >= 0) { key = -1 - type.m_ScriptTypeIndex; } items[key] = new TypeTreeItem(key, type.m_Nodes); } typeMap.Add(assetsFile.unityVersion, items); } } return typeMap; } public static void ExportAssets(string savePath, List toExportAssets, ExportType exportType) { ThreadPool.QueueUserWorkItem(state => { Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US"); int toExportCount = toExportAssets.Count; int exportedCount = 0; int i = 0; Progress.Reset(); foreach (var asset in toExportAssets) { string exportPath; switch (Properties.Settings.Default.assetGroupOption) { case 0: //type name exportPath = Path.Combine(savePath, asset.TypeString); break; case 1: //container path if (!string.IsNullOrEmpty(asset.Container)) { exportPath = Path.Combine(savePath, Path.GetDirectoryName(asset.Container)); } else { exportPath = savePath; } break; case 2: //source file exportPath = Path.Combine(savePath, asset.SourceFile.fullName + "_export"); break; default: exportPath = savePath; break; } exportPath += Path.DirectorySeparatorChar; StatusStripUpdate($"Exporting {asset.TypeString}: {asset.Text}"); try { switch (exportType) { case ExportType.Raw: if (ExportRawFile(asset, exportPath)) { exportedCount++; } break; case ExportType.Dump: if (ExportDumpFile(asset, exportPath)) { exportedCount++; } break; case ExportType.Convert: if (ExportConvertFile(asset, exportPath)) { exportedCount++; } break; } } catch (Exception ex) { MessageBox.Show($"Export {asset.Type}:{asset.Text} error\r\n{ex.Message}\r\n{ex.StackTrace}"); } Progress.Report(++i, toExportCount); } var statusText = exportedCount == 0 ? "Nothing exported." : $"Finished exporting {exportedCount} assets."; if (toExportCount > exportedCount) { statusText += $" {toExportCount - exportedCount} assets skipped (not extractable or files already exist)"; } StatusStripUpdate(statusText); if (Properties.Settings.Default.openAfterExport && exportedCount > 0) { Process.Start(savePath); } }); } public static void ExportSplitObjects(string savePath, TreeNodeCollection nodes) { ThreadPool.QueueUserWorkItem(state => { var count = nodes.Cast().Sum(x => x.Nodes.Count); int k = 0; Progress.Reset(); foreach (GameObjectTreeNode node in nodes) { //遍历一级子节点 foreach (GameObjectTreeNode j in node.Nodes) { //收集所有子节点 var gameObjects = new List(); CollectNode(j, gameObjects); //跳过一些不需要导出的object if (gameObjects.All(x => x.m_SkinnedMeshRenderer == null && x.m_MeshFilter == null)) { Progress.Report(++k, count); continue; } //处理非法文件名 var filename = FixFileName(j.Text); //每个文件存放在单独的文件夹 var targetPath = $"{savePath}{filename}\\"; //重名文件处理 for (int i = 1; ; i++) { if (Directory.Exists(targetPath)) { targetPath = $"{savePath}{filename} ({i})\\"; } else { break; } } Directory.CreateDirectory(targetPath); //导出FBX StatusStripUpdate($"Exporting {filename}.fbx"); try { ExportGameObject(j.gameObject, targetPath); } catch (Exception ex) { MessageBox.Show($"Export GameObject:{j.Text} error\r\n{ex.Message}\r\n{ex.StackTrace}"); } Progress.Report(++k, count); StatusStripUpdate($"Finished exporting {filename}.fbx"); } } if (Properties.Settings.Default.openAfterExport) { Process.Start(savePath); } StatusStripUpdate("Finished"); }); } private static void CollectNode(GameObjectTreeNode node, List gameObjects) { gameObjects.Add(node.gameObject); foreach (GameObjectTreeNode i in node.Nodes) { CollectNode(i, gameObjects); } } public static void ExportAnimatorWithAnimationClip(AssetItem animator, List animationList, string exportPath) { ThreadPool.QueueUserWorkItem(state => { Progress.Reset(); StatusStripUpdate($"Exporting {animator.Text}"); try { ExportAnimator(animator, exportPath, animationList); if (Properties.Settings.Default.openAfterExport) { Process.Start(exportPath); } Progress.Report(1, 1); StatusStripUpdate($"Finished exporting {animator.Text}"); } catch (Exception ex) { MessageBox.Show($"Export Animator:{animator.Text} error\r\n{ex.Message}\r\n{ex.StackTrace}"); StatusStripUpdate("Error in export"); } }); } public static void ExportObjectsWithAnimationClip(string exportPath, TreeNodeCollection nodes, List animationList = null) { ThreadPool.QueueUserWorkItem(state => { var gameObjects = new List(); GetSelectedParentNode(nodes, gameObjects); if (gameObjects.Count > 0) { var count = gameObjects.Count; int i = 0; Progress.Reset(); foreach (var gameObject in gameObjects) { StatusStripUpdate($"Exporting {gameObject.m_Name}"); try { ExportGameObject(gameObject, exportPath, animationList); StatusStripUpdate($"Finished exporting {gameObject.m_Name}"); } catch (Exception ex) { MessageBox.Show($"Export GameObject:{gameObject.m_Name} error\r\n{ex.Message}\r\n{ex.StackTrace}"); StatusStripUpdate("Error in export"); } Progress.Report(++i, count); } if (Properties.Settings.Default.openAfterExport) { Process.Start(exportPath); } } else { StatusStripUpdate("No Object can be exported."); } }); } public static void ExportObjectsMergeWithAnimationClip(string exportPath, List gameObjects, List animationList = null) { ThreadPool.QueueUserWorkItem(state => { var name = Path.GetFileName(exportPath); Progress.Reset(); StatusStripUpdate($"Exporting {name}"); try { ExportGameObjectMerge(gameObjects, exportPath, animationList); Progress.Report(1, 1); StatusStripUpdate($"Finished exporting {name}"); } catch (Exception ex) { MessageBox.Show($"Export Model:{name} error\r\n{ex.Message}\r\n{ex.StackTrace}"); StatusStripUpdate("Error in export"); } if (Properties.Settings.Default.openAfterExport) { Process.Start(Path.GetDirectoryName(exportPath)); } }); } public static void GetSelectedParentNode(TreeNodeCollection nodes, List gameObjects) { foreach (GameObjectTreeNode i in nodes) { if (i.Checked) { gameObjects.Add(i.gameObject); } else { GetSelectedParentNode(i.Nodes, gameObjects); } } } public static string DeserializeMonoBehaviour(MonoBehaviour m_MonoBehaviour) { if (!assemblyLoader.Loaded) { var openFolderDialog = new OpenFolderDialog(); openFolderDialog.Title = "Select Assembly Folder"; if (openFolderDialog.ShowDialog() == DialogResult.OK) { assemblyLoader.Load(openFolderDialog.Folder); } else { assemblyLoader.Loaded = true; } } var nodes = m_MonoBehaviour.ConvertToTypeTreeNode(assemblyLoader); if (nodes != null) { var sb = new StringBuilder(); TypeTreeHelper.ReadTypeString(sb, nodes, m_MonoBehaviour.reader); return sb.ToString(); } return null; } } }