using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Threading; using System.Web.Script.Serialization; using System.Windows.Forms; using static AssetStudio.Exporter; namespace AssetStudio { internal static class Studio { public static List assetsfileList = new List(); //loaded files public static Dictionary sharedFileIndex = new Dictionary(); //to improve the loading speed public static Dictionary resourceFileReaders = new Dictionary(); //use for read res files public static List exportableAssets = new List(); //used to hold all assets while the ListView is filtered private static HashSet exportableAssetsHash = new HashSet(); //avoid the same name asset public static List visibleAssets = new List(); //used to build the ListView from all or filtered assets public static string productName = ""; public static string mainPath = ""; public static List fileNodes = new List(); public static Dictionary> jsonMats; public static Dictionary> AllClassStructures = new Dictionary>(); //UI public static Action SetProgressBarValue; public static Action SetProgressBarMaximum; public static Action ProgressBarPerformStep; public static Action StatusStripUpdate; public static Action ProgressBarMaximumAdd; public static void ExtractBundle(string[] bundleFileName) { ThreadPool.QueueUserWorkItem(state => { int extractedCount = 0; foreach (var fileName in bundleFileName) { extractedCount += ExtractBundleFile(fileName); ProgressBarPerformStep(); } StatusStripUpdate($"Finished extracting {extractedCount} files."); }); } private static int ExtractBundleFile(string bundleFileName) { int extractedCount = 0; var extractPath = bundleFileName + "_unpacked\\"; Directory.CreateDirectory(extractPath); var reader = new EndianBinaryReader(File.OpenRead(bundleFileName)); var bundleFile = new BundleFile(reader); reader.Dispose(); if (bundleFile.fileList.Count > 0) { StatusStripUpdate($"Decompressing {Path.GetFileName(bundleFileName)} ..."); foreach (var memFile in bundleFile.fileList) { var filePath = extractPath + memFile.fileName.Replace('/', '\\'); if (!Directory.Exists(Path.GetDirectoryName(filePath))) { Directory.CreateDirectory(Path.GetDirectoryName(filePath)); } if (!File.Exists(filePath)) { File.WriteAllBytes(filePath, memFile.stream.ToArray()); memFile.stream.Dispose(); extractedCount += 1; } } } return extractedCount; } public static void BuildAssetStructures(bool loadAssetsMenuItem, bool displayAll, bool buildHierarchyMenuItem, bool buildClassStructuresMenuItem, bool displayOriginalName) { #region first loop - read asset data & create list if (loadAssetsMenuItem) { SetProgressBarValue(0); SetProgressBarMaximum(assetsfileList.Sum(x => x.preloadTable.Values.Count)); string fileIDfmt = "D" + assetsfileList.Count.ToString().Length; for (var i = 0; i < assetsfileList.Count; i++) { var assetsFile = assetsfileList[i]; StatusStripUpdate("Building asset list from " + Path.GetFileName(assetsFile.filePath)); string fileID = i.ToString(fileIDfmt); AssetBundle ab = null; foreach (var asset in assetsFile.preloadTable.Values) { asset.uniqueID = fileID + asset.uniqueID; var exportable = false; switch (asset.Type) { case ClassIDReference.GameObject: { GameObject m_GameObject = new GameObject(asset); assetsFile.GameObjectList.Add(asset.m_PathID, m_GameObject); //totalTreeNodes++; break; } case ClassIDReference.Transform: { Transform m_Transform = new Transform(asset); assetsFile.TransformList.Add(asset.m_PathID, m_Transform); break; } case ClassIDReference.RectTransform: { RectTransform m_Rect = new RectTransform(asset); assetsFile.TransformList.Add(asset.m_PathID, m_Rect.m_Transform); break; } case ClassIDReference.Texture2D: { Texture2D m_Texture2D = new Texture2D(asset, false); exportable = true; break; } case ClassIDReference.Shader: { Shader m_Shader = new Shader(asset, false); exportable = true; break; } case ClassIDReference.TextAsset: { TextAsset m_TextAsset = new TextAsset(asset, false); exportable = true; break; } case ClassIDReference.AudioClip: { AudioClip m_AudioClip = new AudioClip(asset, false); exportable = true; break; } case ClassIDReference.MonoBehaviour: { var m_MonoBehaviour = new MonoBehaviour(asset, false); if (asset.Type1 != asset.Type2 && assetsFile.ClassStructures.ContainsKey(asset.Type1)) exportable = true; break; } case ClassIDReference.Font: { UFont m_Font = new UFont(asset, false); exportable = true; break; } case ClassIDReference.PlayerSettings: { var plSet = new PlayerSettings(asset); productName = plSet.productName; break; } case ClassIDReference.Mesh: { Mesh m_Mesh = new Mesh(asset, false); exportable = true; break; } case ClassIDReference.AssetBundle: { ab = new AssetBundle(asset); break; } case ClassIDReference.VideoClip: { var m_VideoClip = new VideoClip(asset, false); exportable = true; break; } case ClassIDReference.MovieTexture: { var m_MovieTexture = new MovieTexture(asset, false); exportable = true; break; } case ClassIDReference.Sprite: { var m_Sprite = new Sprite(asset, false); exportable = true; break; } case ClassIDReference.Animator: { exportable = true; break; } case ClassIDReference.AnimationClip: { exportable = true; var reader = asset.sourceFile.assetsFileReader; reader.Position = asset.Offset; asset.Text = reader.ReadAlignedString(); break; } } if (!exportable && displayAll) { asset.extension = ".dat"; exportable = true; } if (exportable) { if (asset.Text == "") { asset.Text = asset.TypeString + " #" + asset.uniqueID; } asset.SubItems.AddRange(new[] { asset.TypeString, asset.fullSize.ToString() }); //处理同名文件 if (!exportableAssetsHash.Add((asset.TypeString + asset.Text).ToUpper())) { asset.Text += " #" + asset.uniqueID; } //处理非法文件名 asset.Text = FixFileName(asset.Text); assetsFile.exportableAssets.Add(asset); } ProgressBarPerformStep(); } if (displayOriginalName) { assetsFile.exportableAssets.ForEach(x => { var replacename = ab?.m_Container.Find(y => y.second.asset.m_PathID == x.m_PathID)?.first; if (!string.IsNullOrEmpty(replacename)) { var ex = Path.GetExtension(replacename); x.Text = !string.IsNullOrEmpty(ex) ? replacename.Replace(ex, "") : replacename; } }); } exportableAssets.AddRange(assetsFile.exportableAssets); } visibleAssets = exportableAssets; exportableAssetsHash.Clear(); } #endregion #region second loop - build tree structure fileNodes = new List(); if (buildHierarchyMenuItem) { SetProgressBarMaximum(1); SetProgressBarValue(1); SetProgressBarMaximum(assetsfileList.Sum(x => x.GameObjectList.Values.Count) + 1); foreach (var assetsFile in assetsfileList) { StatusStripUpdate("Building tree structure from " + Path.GetFileName(assetsFile.filePath)); GameObject fileNode = new GameObject(null); fileNode.Text = Path.GetFileName(assetsFile.filePath); fileNode.m_Name = "RootNode"; foreach (var m_GameObject in assetsFile.GameObjectList.Values) { //ParseGameObject foreach (var m_Component in m_GameObject.m_Components) { if (m_Component.m_FileID >= 0 && m_Component.m_FileID < assetsfileList.Count) { var sourceFile = assetsfileList[m_Component.m_FileID]; if (sourceFile.preloadTable.TryGetValue(m_Component.m_PathID, out var asset)) { switch (asset.Type) { case ClassIDReference.Transform: { m_GameObject.m_Transform = m_Component; break; } case ClassIDReference.MeshRenderer: { m_GameObject.m_MeshRenderer = m_Component; break; } case ClassIDReference.MeshFilter: { m_GameObject.m_MeshFilter = m_Component; break; } case ClassIDReference.SkinnedMeshRenderer: { m_GameObject.m_SkinnedMeshRenderer = m_Component; break; } case ClassIDReference.Animator: { asset.Text = m_GameObject.asset.Text;//TODO break; } } } } } // var parentNode = fileNode; if (assetsfileList.TryGetTransform(m_GameObject.m_Transform, out var m_Transform)) { if (assetsfileList.TryGetTransform(m_Transform.m_Father, out var m_Father)) { //GameObject Parent; if (assetsfileList.TryGetGameObject(m_Father.m_GameObject, out parentNode)) { //parentNode = Parent; } } } parentNode.Nodes.Add(m_GameObject); ProgressBarPerformStep(); } if (fileNode.Nodes.Count == 0) { fileNode.Text += " (no children)"; } fileNodes.Add(fileNode); } if (File.Exists(mainPath + "\\materials.json")) { string matLine; using (StreamReader reader = File.OpenText(mainPath + "\\materials.json")) { matLine = reader.ReadToEnd(); } jsonMats = new JavaScriptSerializer().Deserialize>>(matLine); //var jsonMats = new JavaScriptSerializer().DeserializeObject(matLine); } } #endregion #region build list of class strucutres if (buildClassStructuresMenuItem) { //group class structures by versionv foreach (var assetsFile in assetsfileList) { if (AllClassStructures.TryGetValue(assetsFile.m_Version, out var curVer)) { foreach (var uClass in assetsFile.ClassStructures) { curVer[uClass.Key] = uClass.Value; } } else { AllClassStructures.Add(assetsFile.m_Version, assetsFile.ClassStructures); } } } #endregion } public static string FixFileName(string str) { if (str.Length >= 260) return Path.GetRandomFileName(); return Path.GetInvalidFileNameChars().Aggregate(str, (current, c) => current.Replace(c, '_')); } public static string[] ProcessingSplitFiles(List selectFile) { var splitFiles = selectFile.Where(x => x.Contains(".split")) .Select(x => Path.GetDirectoryName(x) + "\\" + Path.GetFileNameWithoutExtension(x)) .Distinct() .ToList(); selectFile.RemoveAll(x => x.Contains(".split")); foreach (var file in splitFiles) { if (File.Exists(file)) { selectFile.Add(file); } } return selectFile.Distinct().ToArray(); } public static void ExportAssets(string savePath, List toExportAssets, int assetGroupSelectedIndex, bool openAfterExport) { ThreadPool.QueueUserWorkItem(state => { Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US"); int toExport = toExportAssets.Count; int exportedCount = 0; SetProgressBarValue(0); SetProgressBarMaximum(toExport); foreach (var asset in toExportAssets) { var exportpath = savePath + "\\"; if (assetGroupSelectedIndex == 1) { exportpath += Path.GetFileNameWithoutExtension(asset.sourceFile.filePath) + "_export\\"; } else if (assetGroupSelectedIndex == 0) { exportpath = savePath + "\\" + asset.TypeString + "\\"; } StatusStripUpdate($"Exporting {asset.TypeString}: {asset.Text}"); switch (asset.Type) { case ClassIDReference.Texture2D: if (ExportTexture2D(asset, exportpath, true)) { exportedCount++; } break; case ClassIDReference.AudioClip: if (ExportAudioClip(asset, exportpath)) { exportedCount++; } break; case ClassIDReference.Shader: if (ExportShader(asset, exportpath)) { exportedCount++; } break; case ClassIDReference.TextAsset: if (ExportTextAsset(asset, exportpath)) { exportedCount++; } break; case ClassIDReference.MonoBehaviour: if (ExportMonoBehaviour(asset, exportpath)) { exportedCount++; } break; case ClassIDReference.Font: if (ExportFont(asset, exportpath)) { exportedCount++; } break; case ClassIDReference.Mesh: if (ExportMesh(asset, exportpath)) { exportedCount++; } break; case ClassIDReference.VideoClip: if (ExportVideoClip(asset, exportpath)) { exportedCount++; } break; case ClassIDReference.MovieTexture: if (ExportMovieTexture(asset, exportpath)) { exportedCount++; } break; case ClassIDReference.Sprite: if (ExportSprite(asset, exportpath)) { exportedCount++; } break; case ClassIDReference.Animator: if (ExportAnimator(asset, exportpath)) { exportedCount++; } break; default: if (ExportRawFile(asset, exportpath)) { exportedCount++; } break; } ProgressBarPerformStep(); } var statusText = exportedCount == 0 ? "Nothing exported." : $"Finished exporting {exportedCount} assets."; if (toExport > exportedCount) { statusText += $" {toExport - exportedCount} assets skipped (not extractable or files already exist)"; } StatusStripUpdate(statusText); if (openAfterExport && exportedCount > 0) { Process.Start(savePath); } }); } public static void ExportSplitObjects(string savePath, TreeNodeCollection nodes) { foreach (TreeNode node in nodes) { //遍历一级子节点 foreach (TreeNode j in node.Nodes) { //收集所有子节点 var gameObjects = new List(); CollectNode(j, gameObjects); //跳过一些不需要导出的object if (gameObjects.All(x => x.m_SkinnedMeshRenderer == null && x.m_MeshFilter == null)) continue; //处理非法文件名 var filename = FixFileName(j.Text); //每个文件单独文件夹 var saveName = $"{savePath}{filename}\\{filename}.fbx"; //重名文件处理 for (int i = 1; ; i++) { if (File.Exists(saveName)) { saveName = $"{savePath}{filename} ({i})\\{filename}.fbx"; } else { break; } } Directory.CreateDirectory(Path.GetDirectoryName(saveName)); //导出FBX FBXExporter.WriteFBX(saveName, gameObjects); } ProgressBarPerformStep(); } } private static void CollectNode(TreeNode node, List gameObjects) { gameObjects.Add((GameObject)node); foreach (TreeNode i in node.Nodes) { CollectNode(i, gameObjects); } } public static void ExportAnimatorWithAnimationClip(AssetPreloadData animator, List animationList, string exportPath) { ThreadPool.QueueUserWorkItem(state => { bool result; try { result = ExportAnimator(animator, animationList, exportPath); } catch (Exception ex) { result = false; MessageBox.Show($"{ex.Message}\r\n{ex.StackTrace}"); } StatusStripUpdate(result ? "Successfully exported" : "Nothing exported."); ProgressBarPerformStep(); }); } public static void ExportObjectsWithAnimationClip(GameObject gameObject, List animationList, string exportPath) { bool result; try { result = ExportGameObject(gameObject, animationList, exportPath); } catch (Exception ex) { result = false; MessageBox.Show($"{ex.Message}\r\n{ex.StackTrace}"); } StatusStripUpdate(result ? "Successfully exported" : "Nothing exported."); } } }