using AssetStudio; using CubismLive2DExtractor; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using System.Xml.Linq; using static AssetStudioGUI.Exporter; using Object = AssetStudio.Object; namespace AssetStudioGUI { internal enum GuiColorTheme { System, Light, Dark } internal enum ExportType { Convert, Raw, Dump } internal enum ExportFilter { All, Selected, Filtered } internal enum ExportL2DFilter { All, Selected, SelectedWithFadeList, SelectedWithFade, SelectedWithClips, } internal enum ExportListType { XML } internal enum AssetGroupOption { TypeName, ContainerPath, ContainerPathFull, SourceFileName, SceneHierarchy, } internal enum ListSearchFilterMode { Include, Exclude, RegexName, RegexContainer, } [Flags] internal enum SelectedAssetType { Animator = 0x01, AnimationClip = 0x02, MonoBehaviourMoc = 0x04, MonoBehaviourFade = 0x08, MonoBehaviourFadeLst = 0x10 } 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(); public static Dictionary l2dModelDict = new Dictionary(); private static Dictionary l2dAssetContainers = new Dictionary(); 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 reader = new FileReader(fileName); if (reader.FileType == FileType.BundleFile) extractedCount += ExtractBundleFile(reader, savePath); else if (reader.FileType == FileType.WebFile) extractedCount += ExtractWebDataFile(reader, savePath); else reader.Dispose(); return extractedCount; } private static int ExtractBundleFile(FileReader reader, string savePath) { Logger.Info($"Decompressing {reader.FileName} ..."); Logger.Debug($"Bundle offset: {reader.Position}"); var count = 0; var bundleStream = new OffsetStream(reader); var bundleReader = new FileReader(reader.FullPath, bundleStream); var bundleFile = new BundleFile(bundleReader, assetsManager.ZstdEnabled, assetsManager.SpecifyUnityVersion); var extractPath = Path.Combine(savePath, reader.FileName + "_unpacked"); if (bundleFile.fileList.Length > 0) { count += ExtractStreamFile(extractPath, bundleFile.fileList); } while (bundleFile.IsMultiBundle) { bundleStream.Offset = reader.Position; bundleReader = new FileReader($"{reader.FullPath}_0x{bundleStream.Offset:X}", bundleStream); if (bundleReader.Position > 0) { bundleStream.Offset += bundleReader.Position; bundleReader.FullPath = $"{reader.FullPath}_0x{bundleStream.Offset:X}"; bundleReader.FileName = $"{reader.FileName}_0x{bundleStream.Offset:X}"; } Logger.Info($"[MultiBundle] Decompressing \"{reader.FileName}\" from offset: 0x{bundleStream.Offset:X}.."); bundleFile = new BundleFile(bundleReader, assetsManager.ZstdEnabled, assetsManager.SpecifyUnityVersion); if (bundleFile.fileList.Length > 0) { count += ExtractStreamFile(extractPath, bundleFile.fileList); } } bundleStream.Dispose(); return count; } private static int ExtractWebDataFile(FileReader reader, string savePath) { Logger.Info($"Decompressing {reader.FileName} ..."); var webFile = new WebFile(reader); reader.Dispose(); if (webFile.fileList.Length > 0) { var extractPath = Path.Combine(savePath, reader.FileName + "_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.path); var fileDirectory = Path.GetDirectoryName(filePath); if (!Directory.Exists(fileDirectory)) { Directory.CreateDirectory(fileDirectory); } 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() { Logger.Info("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)>(); var tex2dArrayAssetList = new List(); l2dAssetContainers.Clear(); var i = 0; Progress.Reset(); foreach (var assetsFile in assetsManager.assetsFileList) { var preloadTable = Array.Empty>(); foreach (var asset in assetsFile.Objects) { var assetItem = new AssetItem(asset); objectAssetItemDic.Add(asset, assetItem); assetItem.UniqueID = " #" + i; var exportable = false; switch (asset) { case PreloadData m_PreloadData: preloadTable = m_PreloadData.m_Assets; break; case GameObject m_GameObject: assetItem.Text = m_GameObject.m_Name; if (m_GameObject.CubismModel != null && TryGetCubismMoc(m_GameObject.CubismModel.CubismModelMono, out var mocMono)) { l2dModelDict[mocMono] = m_GameObject.CubismModel; } 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 Texture2DArray m_Texture2DArray: if (!string.IsNullOrEmpty(m_Texture2DArray.m_StreamData?.path)) assetItem.FullSize = asset.byteSize + m_Texture2DArray.m_StreamData.size; assetItem.Text = m_Texture2DArray.m_Name; tex2dArrayAssetList.Add(assetItem); 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 + 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: var assetName = m_MonoBehaviour.m_Name; if (m_MonoBehaviour.m_Script.TryGet(out var m_Script)) { assetName = assetName == "" ? m_Script.m_ClassName : assetName; switch (m_Script.m_ClassName) { case "CubismMoc": if (!l2dModelDict.ContainsKey(m_MonoBehaviour)) { l2dModelDict.Add(m_MonoBehaviour, null); } break; case "CubismRenderer": BindCubismRenderer(m_MonoBehaviour); break; case "CubismDisplayInfoParameterName": BindParamDisplayInfo(m_MonoBehaviour); break; case "CubismDisplayInfoPartName": BindPartDisplayInfo(m_MonoBehaviour); break; case "CubismPosePart": BindCubismPosePart(m_MonoBehaviour); break; } } assetItem.Text = assetName; exportable = true; break; case PlayerSettings m_PlayerSettings: productName = m_PlayerSettings.productName; break; case AssetBundle m_AssetBundle: var isStreamedSceneAssetBundle = m_AssetBundle.m_IsStreamedSceneAssetBundle; if (!isStreamedSceneAssetBundle) { preloadTable = m_AssetBundle.m_PreloadTable; } assetItem.Text = string.IsNullOrEmpty(m_AssetBundle.m_AssetBundleName) ? m_AssetBundle.m_Name : m_AssetBundle.m_AssetBundleName; foreach (var m_Container in m_AssetBundle.m_Container) { var preloadIndex = m_Container.Value.preloadIndex; var preloadSize = isStreamedSceneAssetBundle ? preloadTable.Length : m_Container.Value.preloadSize; var preloadEnd = preloadIndex + preloadSize; for (var k = preloadIndex; k < preloadEnd; k++) { containers.Add((preloadTable[k], m_Container.Key)); } } 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); } asset.Name = assetItem.Text; Progress.Report(++i, objectCount); } } foreach (var (pptr, container) in containers) { if (pptr.TryGet(out var obj)) { objectAssetItemDic[obj].Container = container; switch (obj) { case GameObject m_GameObject: if (m_GameObject.CubismModel != null) { m_GameObject.CubismModel.Container = container; } break; case AnimationClip _: case Texture2D _: case MonoBehaviour _: l2dAssetContainers[obj] = container; break; } } } foreach (var tex2dAssetItem in tex2dArrayAssetList) { var m_Texture2DArray = (Texture2DArray)tex2dAssetItem.Asset; for (var layer = 0; layer < m_Texture2DArray.m_Depth; layer++) { var fakeObj = new Texture2D(m_Texture2DArray, layer); m_Texture2DArray.TextureList.Add(fakeObj); var fakeItem = new AssetItem(fakeObj) { Text = fakeObj.m_Name, Container = tex2dAssetItem.Container }; exportableAssets.Add(fakeItem); } } foreach (var tmp in exportableAssets) { tmp.SetSubItems(); } containers.Clear(); tex2dArrayAssetList.Clear(); visibleAssets = exportableAssets; if (!Properties.Settings.Default.buildTreeStructure) { Logger.Info("Building tree structure step is skipped"); objectAssetItemDic.Clear(); return (productName, new List()); } Logger.Info("Building tree structure..."); var treeNodeCollection = new List(); var treeNodeDictionary = new Dictionary(); var assetsFileCount = assetsManager.assetsFileList.Count; var j = 0; Progress.Reset(); foreach (var assetsFile in assetsManager.assetsFileList) { var fileNode = new TreeNode(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 var parentGameObjectNode)) { parentGameObjectNode = new GameObjectTreeNode(parentGameObject); treeNodeDictionary.Add(parentGameObject, parentGameObjectNode); } parentNode = parentGameObjectNode; } } } 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.version, out var curVer)) { foreach (var type in assetsFile.m_Types.Where(x => x.m_Type != null)) { var key = type.classID; if (type.m_ScriptTypeIndex >= 0) { key = -1 - type.m_ScriptTypeIndex; } curVer[key] = new TypeTreeItem(key, type.m_Type); } } else { var items = new SortedDictionary(); foreach (var type in assetsFile.m_Types.Where(x => x.m_Type != null)) { var key = type.classID; if (type.m_ScriptTypeIndex >= 0) { key = -1 - type.m_ScriptTypeIndex; } items[key] = new TypeTreeItem(key, type.m_Type); } typeMap.Add(assetsFile.version, items); } } return typeMap; } public static void ExportAssets(string savePath, List toExportAssets, ExportType exportType) { ThreadPool.QueueUserWorkItem(state => { Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US"); var groupOption = (AssetGroupOption)Properties.Settings.Default.assetGroupOption; var parallelExportCount = Properties.Settings.Default.parallelExportCount <= 0 ? Environment.ProcessorCount - 1 : Math.Min(Properties.Settings.Default.parallelExportCount, Environment.ProcessorCount - 1); parallelExportCount = Properties.Settings.Default.parallelExport ? parallelExportCount : 1; var toExportAssetDict = new ConcurrentDictionary(); var toParallelExportAssetDict = new ConcurrentDictionary(); var exceptionMsgs = new ConcurrentDictionary(); var mode = exportType == ExportType.Dump ? "Dump" : "Export"; var toExportCount = toExportAssets.Count; var exportedCount = 0; var i = 0; Progress.Reset(); Parallel.ForEach(toExportAssets, asset => { string exportPath; switch (groupOption) { case AssetGroupOption.TypeName: exportPath = Path.Combine(savePath, asset.TypeString); break; case AssetGroupOption.ContainerPath: case AssetGroupOption.ContainerPathFull: if (!string.IsNullOrEmpty(asset.Container)) { exportPath = Path.Combine(savePath, Path.GetDirectoryName(asset.Container)); if (groupOption == AssetGroupOption.ContainerPathFull) { exportPath = Path.Combine(exportPath, Path.GetFileNameWithoutExtension(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; case AssetGroupOption.SceneHierarchy: if (asset.TreeNode != null) { exportPath = Path.Combine(savePath, asset.TreeNode.FullPath); } else { exportPath = Path.Combine(savePath, "_sceneRoot", asset.TypeString); } break; default: exportPath = savePath; break; } exportPath += Path.DirectorySeparatorChar; if (exportType == ExportType.Convert) { switch (asset.Type) { case ClassIDType.Texture2D: case ClassIDType.Texture2DArrayImage: case ClassIDType.Sprite: case ClassIDType.AudioClip: toParallelExportAssetDict.TryAdd(asset, exportPath); break; case ClassIDType.Texture2DArray: var m_Texture2DArray = (Texture2DArray)asset.Asset; toExportCount += m_Texture2DArray.TextureList.Count - 1; foreach (var texture in m_Texture2DArray.TextureList) { var fakeItem = new AssetItem(texture) { Text = texture.m_Name, Container = asset.Container, }; toParallelExportAssetDict.TryAdd(fakeItem, exportPath); } break; default: toExportAssetDict.TryAdd(asset, exportPath); break; } } else { toExportAssetDict.TryAdd(asset, exportPath); } }); foreach (var toExportAsset in toExportAssetDict) { var asset = toExportAsset.Key; var exportPath = toExportAsset.Value; var isExported = false; try { Logger.Info($"[{exportedCount + 1}/{toExportCount}] {mode}ing {asset.TypeString}: {asset.Text}"); switch (exportType) { case ExportType.Raw: isExported = ExportRawFile(asset, exportPath); break; case ExportType.Dump: isExported = ExportDumpFile(asset, exportPath); break; case ExportType.Convert: isExported = ExportConvertFile(asset, exportPath); break; } } catch (Exception ex) { Logger.Error($"{mode} {asset.TypeString}: {asset.Text} error", ex); } if (isExported) { exportedCount++; } else { Logger.Warning($"Unable to {mode.ToLower()} {asset.TypeString}: {asset.Text}"); } Progress.Report(++i, toExportCount); } Parallel.ForEach(toParallelExportAssetDict, new ParallelOptions { MaxDegreeOfParallelism = parallelExportCount }, (toExportAsset, loopState) => { var asset = toExportAsset.Key; var exportPath = toExportAsset.Value; try { if (ParallelExporter.ParallelExportConvertFile(asset, exportPath, out var debugLog)) { Interlocked.Increment(ref exportedCount); if (GUILogger.ShowDebugMessage) { Logger.Debug(debugLog); StatusStripUpdate($"[{exportedCount}/{toExportCount}] Exporting {asset.TypeString}: {asset.Text}"); } else { Logger.Info($"[{exportedCount}/{toExportCount}] Exporting {asset.TypeString}: {asset.Text}"); } } Interlocked.Increment(ref i); Progress.Report(i, toExportCount); } catch (Exception ex) { if (parallelExportCount == 1) { Logger.Error($"{mode} {asset.TypeString}: {asset.Text} error", ex); } else { loopState.Break(); exceptionMsgs.TryAdd(ex, $"Exception occurred when exporting {asset.TypeString}: {asset.Text}\n{ex}\n"); } } }); ParallelExporter.ClearHash(); foreach (var ex in exceptionMsgs) { Logger.Error(ex.Value); } var statusText = exportedCount == 0 ? "Nothing exported." : $"Finished {mode.ToLower()}ing [{exportedCount}/{toExportCount}] assets."; if (toExportCount > exportedCount) { statusText += exceptionMsgs.IsEmpty ? $" {toExportCount - exportedCount} assets skipped (not extractable or files already exist)." : " Export process was stopped because one or more exceptions occurred."; Progress.Report(toExportCount, toExportCount); } Logger.Info(statusText); exceptionMsgs.Clear(); if (Properties.Settings.Default.openAfterExport && exportedCount > 0) { OpenFolderInExplorer(savePath); } }); } public static void ExportAssetsList(string savePath, List toExportAssets, ExportListType exportListType) { ThreadPool.QueueUserWorkItem(state => { Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US"); Progress.Reset(); switch (exportListType) { 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")), toExportAssets.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("TreeNode", asset.TreeNode != null ? asset.TreeNode.FullPath : ""), new XElement("Size", asset.FullSize) ) ) ) ); doc.Save(filename); break; } var statusText = $"Finished exporting asset list with {toExportAssets.Count} items."; Logger.Info(statusText); if (Properties.Settings.Default.openAfterExport && toExportAssets.Count > 0) { OpenFolderInExplorer(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 (TreeNode 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}{Path.DirectorySeparatorChar}"; //重名文件处理 for (int i = 1;; i++) { if (Directory.Exists(targetPath)) { targetPath = $"{savePath}{filename} ({i}){Path.DirectorySeparatorChar}"; } else { break; } } Directory.CreateDirectory(targetPath); //导出FBX Logger.Info($"Exporting {filename}.fbx"); try { ExportGameObject(j.gameObject, targetPath); } catch (Exception ex) { Logger.Error($"Export GameObject:{j.Text} error", ex); } Progress.Report(++k, count); Logger.Info($"Finished exporting {filename}.fbx"); } } if (Properties.Settings.Default.openAfterExport) { OpenFolderInExplorer(savePath); } Logger.Info("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(); Logger.Info($"Exporting {animator.Text}"); Logger.Debug($"Selected AnimationClip(s):\n\"{string.Join("\"\n\"", animationList.Select(x => x.Text))}\""); try { ExportAnimator(animator, exportPath, animationList); if (Properties.Settings.Default.openAfterExport) { OpenFolderInExplorer(exportPath); } Progress.Report(1, 1); Logger.Info($"Finished exporting {animator.Text}"); } catch (Exception ex) { Logger.Error($"Export Animator:{animator.Text} error", ex); Logger.Info("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) { Logger.Info($"Exporting {gameObject.m_Name}"); try { ExportGameObject(gameObject, exportPath, animationList); Logger.Info($"Finished exporting {gameObject.m_Name}"); } catch (Exception ex) { Logger.Error($"Export GameObject:{gameObject.m_Name} error", ex); Logger.Info("Error in export"); } Progress.Report(++i, count); } if (Properties.Settings.Default.openAfterExport) { OpenFolderInExplorer(exportPath); } } else { Logger.Info("No Object selected for export."); } }); } public static void ExportObjectsMergeWithAnimationClip(string exportPath, List gameObjects, List animationList = null) { ThreadPool.QueueUserWorkItem(state => { var name = Path.GetFileName(exportPath); Progress.Reset(); Logger.Info($"Exporting {name}"); try { ExportGameObjectMerge(gameObjects, exportPath, animationList); Progress.Report(1, 1); Logger.Info($"Finished exporting {name}"); } catch (Exception ex) { Logger.Error($"Export Model:{name} error", ex); Logger.Info("Error in export"); } if (Properties.Settings.Default.openAfterExport) { OpenFolderInExplorer(Path.GetDirectoryName(exportPath)); } }); } public static void GetSelectedParentNode(TreeNodeCollection nodes, List gameObjects) { foreach (TreeNode i in nodes) { if (i is GameObjectTreeNode gameObjectTreeNode && i.Checked) { gameObjects.Add(gameObjectTreeNode.gameObject); } else { GetSelectedParentNode(i.Nodes, gameObjects); } } } public static TypeTree MonoBehaviourToTypeTree(MonoBehaviour m_MonoBehaviour) { SelectAssemblyFolder(); return m_MonoBehaviour.ConvertToTypeTree(assemblyLoader); } private static void SelectAssemblyFolder() { 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; } } } public static string DumpAsset(Object obj) { var str = obj.Dump(); if (str == null && obj is MonoBehaviour m_MonoBehaviour) { var type = MonoBehaviourToTypeTree(m_MonoBehaviour); str = m_MonoBehaviour.Dump(type); } if (string.IsNullOrEmpty(str)) { str = obj.DumpObject(); } return str; } public static JsonDocument DumpAssetToJsonDoc(Object obj) { if (obj == null) return null; if (obj is MonoBehaviour m_MonoBehaviour) { var type = obj.serializedType?.m_Type ?? MonoBehaviourToTypeTree(m_MonoBehaviour); return m_MonoBehaviour.ToJsonDoc(type); } return obj.ToJsonDoc(); } public static void OpenFolderInExplorer(string path) { if (!path.EndsWith($"{Path.DirectorySeparatorChar}")) path += Path.DirectorySeparatorChar; if (!Directory.Exists(path)) return; var info = new ProcessStartInfo(path); info.UseShellExecute = true; Process.Start(info); } private static bool TryGetCubismMoc(MonoBehaviour m_MonoBehaviour, out MonoBehaviour mocMono) { mocMono = null; var pptrDict = (OrderedDictionary)CubismParsers.ParseMonoBehaviour(m_MonoBehaviour, CubismParsers.CubismMonoBehaviourType.Model, assemblyLoader)?["_moc"]; if (pptrDict == null) return false; var mocPPtr = new PPtr { m_FileID = (int)pptrDict["m_FileID"], m_PathID = (long)pptrDict["m_PathID"], AssetsFile = m_MonoBehaviour.assetsFile }; return mocPPtr.TryGet(out mocMono); } private static void BindCubismRenderer(MonoBehaviour m_MonoBehaviour) { if (!m_MonoBehaviour.m_GameObject.TryGet(out var m_GameObject)) return; var rootTransform = GetRootTransform(m_GameObject.m_Transform); if (rootTransform.m_GameObject.TryGet(out var rootGameObject) && rootGameObject.CubismModel != null) { rootGameObject.CubismModel.RenderTextureList.Add(m_MonoBehaviour); } } private static void BindParamDisplayInfo(MonoBehaviour m_MonoBehaviour) { if (!m_MonoBehaviour.m_GameObject.TryGet(out var m_GameObject)) return; var rootTransform = GetRootTransform(m_GameObject.m_Transform); if (rootTransform.m_GameObject.TryGet(out var rootGameObject) && rootGameObject.CubismModel != null) { rootGameObject.CubismModel.ParamDisplayInfoList.Add(m_MonoBehaviour); } } private static void BindPartDisplayInfo(MonoBehaviour m_MonoBehaviour) { if (!m_MonoBehaviour.m_GameObject.TryGet(out var m_GameObject)) return; var rootTransform = GetRootTransform(m_GameObject.m_Transform); if (rootTransform.m_GameObject.TryGet(out var rootGameObject) && rootGameObject.CubismModel != null) { rootGameObject.CubismModel.PartDisplayInfoList.Add(m_MonoBehaviour); } } private static void BindCubismPosePart(MonoBehaviour m_MonoBehaviour) { if (!m_MonoBehaviour.m_GameObject.TryGet(out var m_GameObject)) return; var rootTransform = GetRootTransform(m_GameObject.m_Transform); if (rootTransform.m_GameObject.TryGet(out var rootGameObject) && rootGameObject.CubismModel != null) { rootGameObject.CubismModel.PosePartList.Add(m_MonoBehaviour); } } private static Transform GetRootTransform(Transform m_Transform) { if (m_Transform == null) return null; while (m_Transform.m_Father.TryGet(out var m_Father)) { m_Transform = m_Father; } return m_Transform; } private static Dictionary GenerateMocPathDict(Dictionary mocDict, Dictionary assetContainers, bool searchByFilename) { var tempMocPathDict = new Dictionary(); var mocPathDict = new Dictionary(); foreach (var mocMono in l2dModelDict.Keys) { if (!l2dAssetContainers.TryGetValue(mocMono, out var fullContainerPath)) continue; var pathSepIndex = fullContainerPath.LastIndexOf('/'); var basePath = pathSepIndex > 0 ? fullContainerPath.Substring(0, pathSepIndex) : fullContainerPath; tempMocPathDict.Add(mocMono, (fullContainerPath, basePath)); } if (tempMocPathDict.Count > 0) { var basePathSet = tempMocPathDict.Values.Select(x => x.Item2).ToHashSet(); var useFullContainerPath = tempMocPathDict.Count != basePathSet.Count; foreach (var moc in mocDict.Keys) { var mocPath = useFullContainerPath ? tempMocPathDict[moc].Item1 //fullContainerPath : tempMocPathDict[moc].Item2; //basePath if (searchByFilename) { mocPathDict.Add(moc, assetContainers[moc]); if (mocDict.TryGetValue(moc, out var model) && model != null) model.Container = mocPath; } else { mocPathDict.Add(moc, mocPath); } } tempMocPathDict.Clear(); } return mocPathDict; } public static void ExportLive2D(string exportPath, List selMocs = null, List selClipMotions = null, List selFadeMotions = null, MonoBehaviour selFadeLst = null) { var baseDestPath = Path.Combine(exportPath, "Live2DOutput"); var forceBezier = Properties.Settings.Default.l2dForceBezier; var modelGroupOption = Properties.Settings.Default.l2dModelGroupOption; var searchByFilename = Properties.Settings.Default.l2dAssetSearchByFilename; var motionMode = Properties.Settings.Default.l2dMotionMode; if (selClipMotions != null) motionMode = Live2DMotionMode.AnimationClipV2; else if (selFadeMotions != null || selFadeLst != null) motionMode = Live2DMotionMode.MonoBehaviour; var mocDict = selMocs != null ? selMocs.ToDictionary(moc => moc, moc => l2dModelDict[moc]) : l2dModelDict; var l2dContainers = searchByFilename ? new Dictionary() : l2dAssetContainers; ThreadPool.QueueUserWorkItem(state => { Logger.Info("Searching for Live2D assets..."); if (searchByFilename) { foreach (var assetKvp in l2dAssetContainers) { l2dContainers[assetKvp.Key] = Path.GetFileName(assetKvp.Key.assetsFile.originalPath); } } var mocPathDict = GenerateMocPathDict(mocDict, l2dContainers, searchByFilename); var assetDict = new Dictionary>(); foreach (var mocKvp in mocPathDict) { var mocPath = mocKvp.Value; var result = l2dContainers.Select(assetKvp => { if (!assetKvp.Value.Contains(mocPath)) return null; var mocPathSpan = mocPath.AsSpan(); var modelNameFromPath = mocPathSpan.Slice(mocPathSpan.LastIndexOf('/') + 1); #if NET9_0_OR_GREATER foreach (var range in assetKvp.Value.AsSpan().Split('/')) { if (modelNameFromPath.SequenceEqual(assetKvp.Value.AsSpan()[range])) return assetKvp.Key; } #else foreach (var str in assetKvp.Value.Split('/')) { if (modelNameFromPath.SequenceEqual(str.AsSpan())) return assetKvp.Key; } #endif return null; }).Where(x => x != null).ToList(); if (result.Count > 0) { assetDict[mocKvp.Key] = result; } } if (searchByFilename) l2dContainers.Clear(); if (mocDict.Keys.First().serializedType?.m_Type == null && !assemblyLoader.Loaded) { Logger.Warning("Specifying the assembly folder may be needed for proper extraction"); SelectAssemblyFolder(); } var totalModelCount = assetDict.Count; var modelCounter = 0; var parallelExportCount = Properties.Settings.Default.parallelExportCount <= 0 ? Environment.ProcessorCount - 1 : Math.Min(Properties.Settings.Default.parallelExportCount, Environment.ProcessorCount - 1); parallelExportCount = Properties.Settings.Default.parallelExport ? parallelExportCount : 1; Live2DExtractor.MocDict = mocDict; Live2DExtractor.Assembly = assemblyLoader; foreach (var assetGroupKvp in assetDict) { var srcContainer = l2dAssetContainers[assetGroupKvp.Key]; Logger.Info($"[{modelCounter + 1}/{totalModelCount}] Exporting Live2D: \"{srcContainer}\"..."); try { var cubismExtractor = new Live2DExtractor(assetGroupKvp, selClipMotions, selFadeMotions, selFadeLst); string modelPath; switch (modelGroupOption) { case Live2DModelGroupOption.SourceFileName: modelPath = Path.GetFileNameWithoutExtension(cubismExtractor.MocMono.assetsFile.originalPath); break; case Live2DModelGroupOption.ModelName: modelPath = !string.IsNullOrEmpty(cubismExtractor.Model?.Name) ? cubismExtractor.Model.Name : Path.GetFileNameWithoutExtension(cubismExtractor.MocMono.assetsFile.originalPath); break; default: //ContainerPath var container = searchByFilename && cubismExtractor.Model != null ? cubismExtractor.Model.Container : srcContainer; modelPath = Path.HasExtension(container) ? container.Replace(Path.GetExtension(container), "") : container; break; } var destPath = Path.Combine(baseDestPath, modelPath) + Path.DirectorySeparatorChar; cubismExtractor.ExtractCubismModel(destPath, motionMode, forceBezier, parallelExportCount); modelCounter++; } catch (Exception ex) { Logger.Error($"Live2D model export error: \"{srcContainer}\"", ex); } Progress.Report(modelCounter, totalModelCount); } Logger.Info($"Finished exporting [{modelCounter}/{totalModelCount}] Live2D model(s)."); Progress.Report(1, 1); if (Properties.Settings.Default.openAfterExport && modelCounter > 0) { OpenFolderInExplorer(exportPath); } }); } } }