diff --git a/AssetStudio/AssetsHelper.cs b/AssetStudio/AssetsHelper.cs index 5ba2602..0269494 100644 --- a/AssetStudio/AssetsHelper.cs +++ b/AssetStudio/AssetsHelper.cs @@ -4,21 +4,21 @@ using System.Linq; using System.Collections.Generic; using System.Threading; using System.Globalization; -using System.Xml.Linq; using Newtonsoft.Json.Converters; using Newtonsoft.Json; using System.Text.RegularExpressions; +using System.Xml; namespace AssetStudio { public static class AssetsHelper { - public const string CABMapName = "Maps"; + public const string MapName = "Maps"; public static CancellationTokenSource tokenSource = new CancellationTokenSource(); private static string BaseFolder = ""; - private static Dictionary CABMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + private static Dictionary Map = new Dictionary(StringComparer.OrdinalIgnoreCase); private static Dictionary> Offsets = new Dictionary>(); private static AssetsManager assetsManager = new AssetsManager() { Silent = true, SkipProcess = true, ResolveDependencies = false }; @@ -31,14 +31,14 @@ namespace AssetStudio public static string[] GetMaps() { - Directory.CreateDirectory(CABMapName); - var files = Directory.GetFiles(CABMapName, "*.bin", SearchOption.TopDirectoryOnly); + Directory.CreateDirectory(MapName); + var files = Directory.GetFiles(MapName, "*.bin", SearchOption.TopDirectoryOnly); return files.Select(x => Path.GetFileNameWithoutExtension(x)).ToArray(); } public static void Clear() { - CABMap.Clear(); + Map.Clear(); Offsets.Clear(); BaseFolder = string.Empty; @@ -55,7 +55,7 @@ namespace AssetStudio public static bool TryAdd(string name, out string path) { - if (CABMap.TryGetValue(name, out var entry)) + if (Map.TryGetValue(name, out var entry)) { path = Path.Combine(BaseFolder, entry.Path); if (!Offsets.ContainsKey(path)) @@ -80,12 +80,12 @@ namespace AssetStudio return false; } - public static void BuildCABMap(string[] files, string mapName, string baseFolder, Game game) + public static void BuildMap(string[] files, string mapName, string baseFolder, Game game) { Logger.Info($"Processing..."); try { - CABMap.Clear(); + Map.Clear(); var collision = 0; BaseFolder = baseFolder; assetsManager.Game = game; @@ -100,7 +100,7 @@ namespace AssetStudio { if (tokenSource.IsCancellationRequested) { - Logger.Info("Building CABMap has been aborted !!"); + Logger.Info("Building Map has been aborted !!"); return; } var dependencies = assetsFile.m_Externals.Select(x => x.fileName).ToArray(); @@ -111,20 +111,20 @@ namespace AssetStudio Dependencies = dependencies }; - if (CABMap.ContainsKey(assetsFile.fileName)) + if (Map.ContainsKey(assetsFile.fileName)) { collision++; continue; } - CABMap.Add(assetsFile.fileName, entry); + Map.Add(assetsFile.fileName, entry); } Logger.Info($"Processed {Path.GetFileName(file)}"); } assetsManager.Clear(); } - CABMap = CABMap.OrderBy(pair => pair.Key).ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); - var outputFile = Path.Combine(CABMapName, $"{mapName}.bin"); + Map = Map.OrderBy(pair => pair.Key).ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); + var outputFile = Path.Combine(MapName, $"{mapName}.bin"); Directory.CreateDirectory(Path.GetDirectoryName(outputFile)); @@ -132,8 +132,8 @@ namespace AssetStudio using (var writer = new BinaryWriter(binaryFile)) { writer.Write(BaseFolder); - writer.Write(CABMap.Count); - foreach (var kv in CABMap) + writer.Write(Map.Count); + foreach (var kv in Map) { writer.Write(kv.Key); writer.Write(kv.Value.Path); @@ -146,21 +146,243 @@ namespace AssetStudio } } - Logger.Info($"CABMap build successfully !! {collision} collisions found"); + Logger.Info($"Map build successfully !! {collision} collisions found"); } catch (Exception e) { - Logger.Warning($"CABMap was not build, {e}"); + Logger.Warning($"Map was not build, {e}"); } } - public static void LoadMap(string mapName) + public static void BuildBoth(string[] files, string mapName, string baseFolder, Game game, string savePath, ExportListType exportListType, ManualResetEvent resetEvent = null, Regex[] nameFilters = null, Regex[] containerFilters = null) + { + Map.Clear(); + var collision = 0; + BaseFolder = baseFolder; + assetsManager.Game = game; + var assets = new List(); + for (int i = 0; i < files.Length; i++) + { + var file = files[i]; + assetsManager.LoadFiles(file); + if (assetsManager.assetsFileList.Count > 0) + { + var relativePath = Path.GetRelativePath(BaseFolder, file); + var containers = new List<(PPtr, string)>(); + var mihoyoBinDataNames = new List<(PPtr, string)>(); + var objectAssetItemDic = new Dictionary(); + var animators = new List<(PPtr, AssetEntry)>(); + foreach (var assetsFile in assetsManager.assetsFileList) + { + if (tokenSource.IsCancellationRequested) + { + Logger.Info("Building Map has been aborted !!"); + return; + } + var dependencies = assetsFile.m_Externals.Select(x => x.fileName).ToArray(); + var entry = new Entry() + { + Path = relativePath, + Offset = assetsFile.offset, + Dependencies = dependencies + }; + + if (Map.ContainsKey(assetsFile.fileName)) + { + collision++; + continue; + } + Map.Add(assetsFile.fileName, entry); + + foreach (var objInfo in assetsFile.m_Objects) + { + var objectReader = new ObjectReader(assetsFile.reader, assetsFile, objInfo, game); + var obj = new Object(objectReader); + var asset = new AssetEntry() + { + Source = file, + PathID = objectReader.m_PathID, + Type = objectReader.type, + Container = "" + }; + + var exportable = true; + switch (objectReader.type) + { + case ClassIDType.AssetBundle: + var assetBundle = new AssetBundle(objectReader); + foreach (var m_Container in 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((assetBundle.m_PreloadTable[k], m_Container.Key)); + } + } + obj = null; + asset.Name = assetBundle.m_Name; + exportable = false; + break; + case ClassIDType.GameObject: + var gameObject = new GameObject(objectReader); + obj = gameObject; + asset.Name = gameObject.m_Name; + exportable = false; + break; + case ClassIDType.Shader: + asset.Name = objectReader.ReadAlignedString(); + if (string.IsNullOrEmpty(asset.Name)) + { + var m_parsedForm = new SerializedShader(objectReader); + asset.Name = m_parsedForm.m_Name; + } + break; + case ClassIDType.Animator: + var component = new PPtr(objectReader); + animators.Add((component, asset)); + break; + case ClassIDType.MiHoYoBinData: + var MiHoYoBinData = new MiHoYoBinData(objectReader); + obj = MiHoYoBinData; + exportable = true; + break; + case ClassIDType.IndexObject: + var indexObject = new IndexObject(objectReader); + obj = null; + foreach (var index in indexObject.AssetMap) + { + mihoyoBinDataNames.Add((index.Value.Object, index.Key)); + } + asset.Name = "IndexObject"; + break; + case ClassIDType.Font: + case ClassIDType.Material: + case ClassIDType.Texture: + case ClassIDType.Mesh: + case ClassIDType.Sprite: + case ClassIDType.TextAsset: + case ClassIDType.Texture2D: + case ClassIDType.VideoClip: + case ClassIDType.AudioClip: + asset.Name = objectReader.ReadAlignedString(); + break; + default: + exportable = false; + break; + } + if (obj != null) + { + objectAssetItemDic.Add(obj, asset); + assetsFile.AddObject(obj); + } + var isMatchRegex = nameFilters.IsNullOrEmpty() || nameFilters.Any(x => x.IsMatch(asset.Name) || asset.Type == ClassIDType.Animator); + if (isMatchRegex && exportable) + { + assets.Add(asset); + } + } + } + foreach ((var pptr, var asset) in animators) + { + if (pptr.TryGet(out var gameObject) && (nameFilters.IsNullOrEmpty() || nameFilters.Any(x => x.IsMatch(gameObject.m_Name)))) + { + asset.Name = gameObject.m_Name; + } + } + foreach ((var pptr, var name) in mihoyoBinDataNames) + { + if (pptr.TryGet(out var miHoYoBinData)) + { + var asset = objectAssetItemDic[miHoYoBinData]; + if (int.TryParse(name, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hash)) + { + asset.Name = name; + asset.Container = hash.ToString(); + } + else asset.Name = $"BinFile #{asset.PathID}"; + } + } + foreach ((var pptr, var container) in containers) + { + if (pptr.TryGet(out var obj)) + { + var item = objectAssetItemDic[obj]; + if (containerFilters.IsNullOrEmpty() || containerFilters.Any(x => x.IsMatch(container))) + { + item.Container = container; + } + else + { + assets.Remove(item); + } + } + } + Logger.Info($"Processed {Path.GetFileName(file)}"); + } + assetsManager.Clear(); + } + if (game.Type.IsGISubGroup() && assets.Count > 0) + { + Logger.Info("Updating Containers..."); + foreach (var asset in assets) + { + if (int.TryParse(asset.Container, out var value)) + { + var last = unchecked((uint)value); + var name = Path.GetFileNameWithoutExtension(asset.Source); + if (uint.TryParse(name, out var id)) + { + var path = ResourceIndex.GetContainer(id, last); + if (!string.IsNullOrEmpty(path)) + { + asset.Container = path; + if (asset.Type == ClassIDType.MiHoYoBinData) + { + asset.Name = Path.GetFileNameWithoutExtension(path); + } + } + } + } + } + Logger.Info("Updated !!"); + } + + Map = Map.OrderBy(pair => pair.Key).ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.OrdinalIgnoreCase); + var outputFile = Path.Combine(MapName, $"{mapName}.bin"); + + Directory.CreateDirectory(Path.GetDirectoryName(outputFile)); + + using (var binaryFile = File.OpenWrite(outputFile)) + using (var writer = new BinaryWriter(binaryFile)) + { + writer.Write(BaseFolder); + writer.Write(Map.Count); + foreach (var kv in Map) + { + writer.Write(kv.Key); + writer.Write(kv.Value.Path); + writer.Write(kv.Value.Offset); + writer.Write(kv.Value.Dependencies.Length); + foreach (var cab in kv.Value.Dependencies) + { + writer.Write(cab); + } + } + } + + Logger.Info($"Map build successfully !! {collision} collisions found"); + ExportAssetsMap(assets.ToArray(), mapName, savePath, exportListType, resetEvent); + } + + public static bool LoadMap(string mapName) { Logger.Info($"Loading {mapName}"); try { - CABMap.Clear(); - using (var fs = File.OpenRead(Path.Combine(CABMapName, $"{mapName}.bin"))) + Map.Clear(); + using (var fs = File.OpenRead(Path.Combine(MapName, $"{mapName}.bin"))) using (var reader = new BinaryReader(fs)) { BaseFolder = reader.ReadString(); @@ -183,18 +405,21 @@ namespace AssetStudio Offset = offset, Dependencies = dependencies.ToArray() }; - CABMap.Add(cab, entry); + Map.Add(cab, entry); } } Logger.Info($"Loaded {mapName} !!"); + return true; } catch (Exception e) { - Logger.Warning($"{mapName} was not loaded, {e}"); + Logger.Warning($"{mapName} was not loaded, {e}"); } + + return false; } - public static AssetEntry[] BuildAssetMap(string[] files, Game game, ClassIDType[] typeFilters = null, Regex[] nameFilters = null, Regex[] containerFilters = null) + public static AssetEntry[] BuildAssetMap(string[] files, Game game, Regex[] nameFilters = null, Regex[] containerFilters = null) { assetsManager.Game = game; var assets = new List(); @@ -210,8 +435,6 @@ namespace AssetStudio var animators = new List<(PPtr, AssetEntry)>(); foreach (var assetsFile in assetsManager.assetsFileList) { - assetsFile.m_Objects = ObjectInfo.Filter(assetsFile.m_Objects); - foreach (var objInfo in assetsFile.m_Objects) { var objectReader = new ObjectReader(assetsFile.reader, assetsFile, objInfo, game); @@ -264,7 +487,7 @@ namespace AssetStudio case ClassIDType.MiHoYoBinData: var MiHoYoBinData = new MiHoYoBinData(objectReader); obj = MiHoYoBinData; - exportable = MiHoYoBinData.Exportable; + exportable = true; break; case ClassIDType.IndexObject: var indexObject = new IndexObject(objectReader); @@ -275,9 +498,20 @@ namespace AssetStudio } asset.Name = "IndexObject"; break; - default: + case ClassIDType.Font: + case ClassIDType.Material: + case ClassIDType.Texture: + case ClassIDType.Mesh: + case ClassIDType.Sprite: + case ClassIDType.TextAsset: + case ClassIDType.Texture2D: + case ClassIDType.VideoClip: + case ClassIDType.AudioClip: asset.Name = objectReader.ReadAlignedString(); break; + default: + exportable = false; + break; } if (obj != null) { @@ -285,8 +519,7 @@ namespace AssetStudio assetsFile.AddObject(obj); } var isMatchRegex = nameFilters.IsNullOrEmpty() || nameFilters.Any(x => x.IsMatch(asset.Name) || asset.Type == ClassIDType.Animator); - var isFilteredType = typeFilters.IsNullOrEmpty() || typeFilters.Contains(asset.Type) || asset.Type == ClassIDType.Animator; - if (isMatchRegex && isFilteredType && exportable) + if (isMatchRegex && exportable) { assets.Add(asset); } @@ -294,7 +527,7 @@ namespace AssetStudio } foreach ((var pptr, var asset) in animators) { - if (pptr.TryGet(out var gameObject) && (nameFilters.IsNullOrEmpty() || nameFilters.Any(x => x.IsMatch(gameObject.m_Name))) && (typeFilters.IsNullOrEmpty() || typeFilters.Contains(asset.Type))) + if (pptr.TryGet(out var gameObject) && (nameFilters.IsNullOrEmpty() || nameFilters.Any(x => x.IsMatch(gameObject.m_Name)))) { asset.Name = gameObject.m_Name; } @@ -339,23 +572,27 @@ namespace AssetStudio if (int.TryParse(asset.Container, out var value)) { var last = unchecked((uint)value); - var path = ResourceIndex.GetAssetPath(last); - if (!string.IsNullOrEmpty(path)) + var name = Path.GetFileNameWithoutExtension(asset.Source); + if (uint.TryParse(name, out var id)) { - asset.Container = path; - if (asset.Type == ClassIDType.MiHoYoBinData) + var path = ResourceIndex.GetContainer(id, last); + if (!string.IsNullOrEmpty(path)) { - asset.Name = Path.GetFileNameWithoutExtension(path); + asset.Container = path; + if (asset.Type == ClassIDType.MiHoYoBinData) + { + asset.Name = Path.GetFileNameWithoutExtension(path); + } } } } + Logger.Info("Updated !!"); } - Logger.Info("Updated !!"); } return assets.ToArray(); } - public static void ExportAssetsMap(AssetEntry[] toExportAssets, string name, string savePath, ExportListType exportListType) + public static void ExportAssetsMap(AssetEntry[] toExportAssets, string name, string savePath, ExportListType exportListType, ManualResetEvent resetEvent = null) { ThreadPool.QueueUserWorkItem(state => { @@ -368,28 +605,35 @@ namespace AssetStudio { case ExportListType.XML: filename = Path.Combine(savePath, $"{name}.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.Name), - new XElement("Container", asset.Container), - new XElement("Type", new XAttribute("id", (int)asset.Type), asset.Type.ToString()), - new XElement("PathID", asset.PathID), - new XElement("Source", asset.Source) - ) - ) - ) - ); - doc.Save(filename); + var settings = new XmlWriterSettings() { Indent = true }; + using (XmlWriter writer = XmlWriter.Create(filename, settings)) + { + writer.WriteStartDocument(); + writer.WriteStartElement("Assets"); + writer.WriteAttributeString("filename", filename); + writer.WriteAttributeString("createdAt", DateTime.UtcNow.ToString("s")); + foreach(var asset in toExportAssets) + { + writer.WriteStartElement("Asset"); + writer.WriteElementString("Name", asset.Name); + writer.WriteElementString("Container", asset.Container); + writer.WriteStartElement("Type"); + writer.WriteAttributeString("id", ((int)asset.Type).ToString()); + writer.WriteValue(asset.Type.ToString()); + writer.WriteEndElement(); + writer.WriteElementString("PathID", asset.PathID.ToString()); + writer.WriteElementString("Source", asset.Source); + writer.WriteEndElement(); + } + writer.WriteEndElement(); + writer.WriteEndDocument(); + } break; case ExportListType.JSON: filename = Path.Combine(savePath, $"{name}.json"); using (StreamWriter file = File.CreateText(filename)) { - var serializer = new JsonSerializer() { Formatting = Formatting.Indented }; + var serializer = new JsonSerializer() { Formatting = Newtonsoft.Json.Formatting.Indented }; serializer.Converters.Add(new StringEnumConverter()); serializer.Serialize(file, toExportAssets); } @@ -398,6 +642,8 @@ namespace AssetStudio Logger.Info($"Finished exporting asset list with {toExportAssets.Length} items."); Logger.Info($"AssetMap build successfully !!"); + + resetEvent?.Set(); }); } } diff --git a/AssetStudio/AssetsManager.cs b/AssetStudio/AssetsManager.cs index 3903159..56b7859 100644 --- a/AssetStudio/AssetsManager.cs +++ b/AssetStudio/AssetsManager.cs @@ -19,15 +19,34 @@ namespace AssetStudio public string SpecifyUnityVersion; public CancellationTokenSource tokenSource = new CancellationTokenSource(); public List assetsFileList = new List(); - public Dictionary ExportableTypes = new() { { ClassIDType.GameObject, true }, { ClassIDType.Material, true }, { ClassIDType.Texture2D, true }, { ClassIDType.Mesh, true }, { ClassIDType.Renderer, true }, { ClassIDType.Shader, true }, { ClassIDType.TextAsset, true }, { ClassIDType.AnimationClip, true }, { ClassIDType.Font, true }, { ClassIDType.Sprite, true }, { ClassIDType.Animator, true }, { ClassIDType.MiHoYoBinData, true }, { ClassIDType.AssetBundle, true } }; + public Dictionary ExportableTypes = new Dictionary(); internal Dictionary assetsFileIndexCache = new Dictionary(StringComparer.OrdinalIgnoreCase); internal Dictionary resourceFileReaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - private List importFiles = new List(); - private HashSet importFilesHash = new HashSet(StringComparer.OrdinalIgnoreCase); - private HashSet noexistFiles = new HashSet(StringComparer.OrdinalIgnoreCase); - private HashSet assetsFileListHash = new HashSet(StringComparer.OrdinalIgnoreCase); + internal List importFiles = new List(); + internal HashSet importFilesHash = new HashSet(StringComparer.OrdinalIgnoreCase); + internal HashSet noexistFiles = new HashSet(StringComparer.OrdinalIgnoreCase); + internal HashSet assetsFileListHash = new HashSet(StringComparer.OrdinalIgnoreCase); + + public AssetsManager() + { + ExportableTypes.Add(ClassIDType.GameObject, true); + ExportableTypes.Add(ClassIDType.Material, true); + ExportableTypes.Add(ClassIDType.Texture2D, true); + ExportableTypes.Add(ClassIDType.AudioClip, true); + ExportableTypes.Add(ClassIDType.VideoClip, true); + ExportableTypes.Add(ClassIDType.Mesh, false); + ExportableTypes.Add(ClassIDType.Renderer, false); + ExportableTypes.Add(ClassIDType.Shader, true); + ExportableTypes.Add(ClassIDType.TextAsset, true); + ExportableTypes.Add(ClassIDType.AnimationClip, true); + ExportableTypes.Add(ClassIDType.MonoBehaviour, true); + ExportableTypes.Add(ClassIDType.Font, true); + ExportableTypes.Add(ClassIDType.Sprite, true); + ExportableTypes.Add(ClassIDType.Animator, true); + ExportableTypes.Add(ClassIDType.MiHoYoBinData, true); + } public void LoadFiles(params string[] files) { @@ -88,6 +107,11 @@ namespace AssetStudio Logger.Info("Loading files has been aborted !!"); break; } + if (!SkipProcess && !tokenSource.IsCancellationRequested) + { + ReadAssets(); + ProcessAssets(); + } } importFiles.Clear(); @@ -96,12 +120,12 @@ namespace AssetStudio assetsFileListHash.Clear(); AssetsHelper.ClearOffsets(); - if (!SkipProcess && !tokenSource.IsCancellationRequested) - { - ReadAssets(); - ProcessAssets(); + //if (!SkipProcess && !tokenSource.IsCancellationRequested) + //{ + // ReadAssets(); + // ProcessAssets(); + //} } - } private void LoadFile(string fullName) { @@ -153,8 +177,6 @@ namespace AssetStudio assetsFileList.Add(assetsFile); assetsFileListHash.Add(assetsFile.fileName); - if (ResolveDependencies) - { foreach (var sharedFile in assetsFile.m_Externals) { var sharedFileName = sharedFile.fileName; @@ -185,7 +207,6 @@ namespace AssetStudio } } } - } catch (Exception e) { Logger.Error($"Error while reading assets file {reader.FullPath}", e); @@ -216,37 +237,37 @@ namespace AssetStudio assetsFileList.Add(assetsFile); assetsFileListHash.Add(assetsFile.fileName); - if (ResolveDependencies) - { - foreach (var sharedFile in assetsFile.m_Externals) - { - var sharedFileName = sharedFile.fileName; - - if (!importFilesHash.Contains(sharedFileName)) - { - var sharedFilePath = Path.Combine(Path.GetDirectoryName(originalPath), sharedFileName); - if (!noexistFiles.Contains(sharedFilePath)) - { - if (AssetsHelper.TryAdd(sharedFileName, out var path)) - { - sharedFilePath = path; - } - if (File.Exists(sharedFilePath)) - { - if (!importFiles.Contains(sharedFilePath)) - { - importFiles.Add(sharedFilePath); - } - importFilesHash.Add(sharedFileName); - } - else - { - noexistFiles.Add(sharedFilePath); - } - } - } - } - } + //if (ResolveDependencies) + //{ + // foreach (var sharedFile in assetsFile.m_Externals) + // { + // var sharedFileName = sharedFile.fileName; + // + // if (!importFilesHash.Contains(sharedFileName)) + // { + // var sharedFilePath = Path.Combine(Path.GetDirectoryName(originalPath), sharedFileName); + // if (!noexistFiles.Contains(sharedFilePath)) + // { + // if (AssetsHelper.TryAdd(sharedFileName, out var path)) + // { + // sharedFilePath = path; + // } + // if (File.Exists(sharedFilePath)) + // { + // if (!importFiles.Contains(sharedFilePath)) + // { + // importFiles.Add(sharedFilePath); + // } + // importFilesHash.Add(sharedFileName); + // } + // else + // { + // noexistFiles.Add(sharedFilePath); + // } + // } + // } + // } + //} } catch (Exception e) { @@ -618,6 +639,10 @@ namespace AssetStudio Logger.Info("Reading assets has been aborted !!"); return; } + if (assetsFile.IsLoaded(objectInfo)) + { + continue; + } var objectReader = new ObjectReader(assetsFile.reader, assetsFile, objectInfo, Game); try { @@ -639,10 +664,10 @@ namespace AssetStudio case ClassIDType.AnimatorOverrideController: obj = new AnimatorOverrideController(objectReader); break; - case ClassIDType.AssetBundle when ExportableTypes[ClassIDType.AssetBundle]: + case ClassIDType.AssetBundle: obj = new AssetBundle(objectReader); break; - case ClassIDType.AudioClip: + case ClassIDType.AudioClip when ExportableTypes[ClassIDType.AudioClip]: obj = new AudioClip(objectReader); break; case ClassIDType.Avatar: @@ -672,7 +697,7 @@ namespace AssetStudio case ClassIDType.MiHoYoBinData when ExportableTypes[ClassIDType.MiHoYoBinData]: obj = new MiHoYoBinData(objectReader); break; - case ClassIDType.MonoBehaviour: + case ClassIDType.MonoBehaviour when ExportableTypes[ClassIDType.MonoBehaviour]: obj = new MonoBehaviour(objectReader); break; case ClassIDType.MonoScript: @@ -708,7 +733,7 @@ namespace AssetStudio case ClassIDType.Transform: obj = new Transform(objectReader); break; - case ClassIDType.VideoClip: + case ClassIDType.VideoClip when ExportableTypes[ClassIDType.VideoClip]: obj = new VideoClip(objectReader); break; case ClassIDType.ResourceManager: diff --git a/AssetStudio/Classes/GameObject.cs b/AssetStudio/Classes/GameObject.cs index d129beb..0196118 100644 --- a/AssetStudio/Classes/GameObject.cs +++ b/AssetStudio/Classes/GameObject.cs @@ -33,5 +33,7 @@ namespace AssetStudio var m_Layer = reader.ReadInt32(); m_Name = reader.ReadAlignedString(); } + + public bool HasModel() => m_Transform != null && m_Transform.m_Father.IsNull && m_Transform.m_Children.Length > 0; } } diff --git a/AssetStudio/Classes/PPtr.cs b/AssetStudio/Classes/PPtr.cs index b31de67..54529ed 100644 --- a/AssetStudio/Classes/PPtr.cs +++ b/AssetStudio/Classes/PPtr.cs @@ -41,6 +41,26 @@ namespace AssetStudio var name = m_External.fileName; if (!assetsFileIndexCache.TryGetValue(name, out index)) { + if (assetsManager.ResolveDependencies && !assetsManager.importFilesHash.Contains(name)) + { + var sharedFilePath = Path.Combine(Path.GetDirectoryName(assetsFile.originalPath), name); + if (!assetsManager.noexistFiles.Contains(sharedFilePath)) + { + if (TryAdd(name, out var path)) + { + sharedFilePath = path; + } + if (File.Exists(sharedFilePath)) + { + assetsManager.importFilesHash.Add(name); + assetsManager.LoadFiles(sharedFilePath); + } + else + { + assetsManager.noexistFiles.Add(sharedFilePath); + } + } + } index = assetsFileList.FindIndex(x => x.fileName.Equals(name, StringComparison.OrdinalIgnoreCase)); assetsFileIndexCache.Add(name, index); } diff --git a/AssetStudio/Crypto/OPFPUtils.cs b/AssetStudio/Crypto/OPFPUtils.cs index 0e245d8..61a02a0 100644 --- a/AssetStudio/Crypto/OPFPUtils.cs +++ b/AssetStudio/Crypto/OPFPUtils.cs @@ -4,7 +4,7 @@ namespace AssetStudio { public static class OPFPUtils { - public static readonly string[] EncrytpedFolders = { "UI/", "Atlas/", "UITexture/", "DynamicAtlas/" }; + public static readonly string[] EncrytpedFolders = { "UITexture", "DynamicAtlas", "UI", "Atlas" }; public static void Decrypt(Span data, string path) { diff --git a/AssetStudio/FileReader.cs b/AssetStudio/FileReader.cs index c7d03ec..9448599 100644 --- a/AssetStudio/FileReader.cs +++ b/AssetStudio/FileReader.cs @@ -157,6 +157,9 @@ namespace AssetStudio case GameType.ShiningNikki: reader = ParseShiningNikki(reader); break; + case GameType.HelixWaltz2: + reader = ParseHelixWaltz2(reader); + break; } } if (reader.FileType == FileType.BundleFile && game.Type.IsBlockFile()) diff --git a/AssetStudio/GameManager.cs b/AssetStudio/GameManager.cs index 0c45af2..1b018f4 100644 --- a/AssetStudio/GameManager.cs +++ b/AssetStudio/GameManager.cs @@ -29,6 +29,7 @@ namespace AssetStudio Games.Add(index++, new Game(GameType.AlchemyStars)); Games.Add(index++, new Game(GameType.FantasyOfWind)); Games.Add(index++, new Game(GameType.ShiningNikki)); + Games.Add(index++, new Game(GameType.HelixWaltz2)); } public static Game GetGame(GameType gameType) => GetGame((int)gameType); public static Game GetGame(int index) @@ -128,7 +129,8 @@ namespace AssetStudio OPFP, AlchemyStars, FantasyOfWind, - ShiningNikki + ShiningNikki, + HelixWaltz2 } public static class GameTypes diff --git a/AssetStudio/ImportHelper.cs b/AssetStudio/ImportHelper.cs index a672f9f..1c14f2f 100644 --- a/AssetStudio/ImportHelper.cs +++ b/AssetStudio/ImportHelper.cs @@ -374,5 +374,52 @@ namespace AssetStudio var stream = new BlockStream(reader.BaseStream, idx); return new FileReader(reader.FullPath, stream); } + public static FileReader ParseHelixWaltz2(FileReader reader) + { + var originalHeader = new byte[] { 0x55, 0x6E, 0x69, 0x74, 0x79, 0x46, 0x53, 0x00, 0x00, 0x00, 0x00, 0x07, 0x35, 0x2E, 0x78, 0x2E }; + + var signature = reader.ReadStringToNull(); + reader.AlignStream(); + + if (signature != "SzxFS") + { + reader.Position = 0; + return reader; + } + + var seed = reader.ReadInt32(); + reader.Position = 0x10; + var data = reader.ReadBytes((int)reader.Remaining); + + var sbox = new byte[0x100]; + for (int i = 0; i < sbox.Length; i++) + { + sbox[i] = (byte)i; + } + + var key = new byte[0x100]; + var random = new Random(seed); + for (int i = 0; i < key.Length; i++) + { + var idx = random.Next(i, 0x100); + var b = sbox[idx]; + sbox[idx] = sbox[i]; + sbox[i] = b; + key[b] = (byte)i; + } + + for (int i = 0; i < data.Length; i++) + { + var idx = data[i]; + data[i] = key[idx]; + } + + MemoryStream ms = new(); + ms.Write(originalHeader); + ms.Write(data); + ms.Position = 0; + + return new FileReader(reader.FullPath, ms); + } } } diff --git a/AssetStudio/ObjectInfo.cs b/AssetStudio/ObjectInfo.cs index 1f51ab8..23cace4 100644 --- a/AssetStudio/ObjectInfo.cs +++ b/AssetStudio/ObjectInfo.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.Linq; - -namespace AssetStudio +namespace AssetStudio { public class ObjectInfo { @@ -14,20 +11,5 @@ namespace AssetStudio public long m_PathID; public SerializedType serializedType; - - public static List Filter(List objects) => objects.Where(x => x.IsExportableType()).OrderBy(x => ExportableTypes.IndexOf((ClassIDType)x.typeID)).ToList(); - - private bool IsExportableType() - { - var typeID = (ClassIDType)classID; - var isExportableType = ExportableTypes.Contains(typeID); - return typeID switch - { - ClassIDType.IndexObject or ClassIDType.MiHoYoBinData => isExportableType && MiHoYoBinData.Exportable, - _ => isExportableType, - }; - } - - private readonly static List ExportableTypes = new List { ClassIDType.GameObject, ClassIDType.IndexObject, ClassIDType.Material, ClassIDType.Texture2D, ClassIDType.Mesh, ClassIDType.Shader, ClassIDType.TextAsset, ClassIDType.AnimationClip, ClassIDType.Font, ClassIDType.Sprite, ClassIDType.Animator, ClassIDType.MiHoYoBinData, ClassIDType.AssetBundle }; } } diff --git a/AssetStudio/ResourceIndex.cs b/AssetStudio/ResourceIndex.cs index 8f454cc..bdbe8b6 100644 --- a/AssetStudio/ResourceIndex.cs +++ b/AssetStudio/ResourceIndex.cs @@ -9,26 +9,8 @@ namespace AssetStudio { public static class ResourceIndex { - public static Dictionary> BundleDependencyMap; - public static Dictionary BlockInfoMap; - public static Dictionary BlockMap; - public static Dictionary AssetMap; - public static List> AssetLocationMap; - public static List BlockSortList; - - static ResourceIndex() - { - BlockSortList = new List(); - AssetMap = new Dictionary(); - AssetLocationMap = new List>(0x100); - for (int i = 0; i < AssetLocationMap.Capacity; i++) - { - AssetLocationMap.Add(new Dictionary(0x1FF)); - } - BundleDependencyMap = new Dictionary>(); - BlockInfoMap = new Dictionary(); - BlockMap = new Dictionary(); - } + private static AssetIndex Instance = new(); + private static Dictionary> BundleMap = new Dictionary>(); public static void FromFile(string path) { if (!string.IsNullOrEmpty(path)) @@ -48,11 +30,10 @@ namespace AssetStudio var json = Encoding.UTF8.GetString(bytes); - var obj = JsonConvert.DeserializeObject(json); - if (obj != null) - { - MapToResourceIndex(obj); - } + var settings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }; + JsonConvert.PopulateObject(json, Instance, settings); + + BuildBundleMap(); } } catch (Exception e) @@ -64,117 +45,52 @@ namespace AssetStudio Logger.Info("Loaded !!"); } } - public static void Clear() + private static void BuildBundleMap() { - BundleDependencyMap.Clear(); - BlockInfoMap.Clear(); - BlockMap.Clear(); - AssetMap.Clear(); - AssetLocationMap.ForEach(x => x.Clear()); - BlockSortList.Clear(); - } - public static void MapToResourceIndex(AssetIndex assetIndex) - { - BundleDependencyMap = assetIndex.Dependencies; - BlockSortList = assetIndex.SortList.ConvertAll(x => (int)x); - foreach (var asset in assetIndex.SubAssets) + foreach(var asset in Instance.Assets) { - foreach (var subAsset in asset.Value) + if (!BundleMap.ContainsKey(asset.Value.Id)) { - var bundleInfo = new BundleInfo() { Bundle = asset.Key, Path = subAsset.Name }; - AssetLocationMap[subAsset.PathHashPre].Add(subAsset.PathHashLast, bundleInfo); - AssetMap[subAsset.PathHashLast] = ((ulong)subAsset.PathHashLast) << 8 | subAsset.PathHashPre; + BundleMap[asset.Value.Id] = new Dictionary(); + } + if (Instance.SubAssets.TryGetValue(asset.Key, out var subAssets)) + { + foreach(var subAsset in subAssets) + { + BundleMap[asset.Value.Id].Add(subAsset.PathHashLast, subAsset.Name); + } } } - foreach (var asset in assetIndex.Assets) + } + public static void Clear() + { + Instance.Types.Clear(); + Instance.SubAssets.Clear(); + Instance.Dependencies.Clear(); + Instance.PreloadBlocks.Clear(); + Instance.PreloadShaderBlocks.Clear(); + Instance.Assets.Clear(); + Instance.SortList.Clear(); + BundleMap.Clear(); + } + public static string GetContainer(uint id, uint last) + { + if (BundleMap.TryGetValue(id, out var bundles)) { - var block = new Block() { Id = (int)asset.Value.Id, Offset = (int)asset.Value.Offset }; - BlockInfoMap.Add(asset.Key, block); + if (bundles.TryGetValue(last, out var container)) + { + return container; + } + } - if (!BlockMap.ContainsKey((int)asset.Value.Id)) - BlockMap.Add((int)asset.Value.Id, asset.Value.Language); - } + return string.Empty; } - public static List GetAllAssets() => AssetLocationMap.SelectMany(x => x.Values).ToList(); - public static List GetAssets(int bundle) => AssetLocationMap.SelectMany(x => x.Values).Where(x => x.Bundle == bundle).ToList(); - public static ulong GetAssetIndex(ulong blkHash) => AssetMap.TryGetValue(blkHash, out var value) ? value : 0; - public static Block GetBlockInfo(int bundle) => BlockInfoMap.TryGetValue(bundle, out var blk) ? blk : null; - public static BlockFile GetBlockFile(int id) => BlockMap.TryGetValue(id, out var languageCode) ? new BlockFile() { LanguageCode = languageCode, Id = id } : null; - public static int GetBlockID(int bundle) => BlockInfoMap.TryGetValue(bundle, out var block) ? block.Id : 0; - public static List GetBundleDep(int bundle) => BundleDependencyMap.TryGetValue(bundle, out var dep) ? dep : new List(); - public static BundleInfo GetBundleInfo(ulong hash) - { - var asset = new Asset() { Hash = hash }; - if (AssetLocationMap.ElementAtOrDefault(asset.Pre) != null) - if (AssetLocationMap[asset.Pre].TryGetValue(asset.Last, out var bundleInfo)) - return bundleInfo; - return null; - } - public static string GetAssetPath(uint last) - { - foreach (var location in AssetLocationMap) - if (location.TryGetValue(last, out var bundleInfo)) - return bundleInfo.Path; - return null; - } - public static List GetAllAssetIndices(int bundle) - { - var hashes = new List(); - foreach (var location in AssetLocationMap) - foreach (var pair in location) - if (pair.Value.Bundle == bundle) - hashes.Add(pair.Key); - return hashes; - } - public static List GetBundles(int id) - { - var bundles = new List(); - foreach (var block in BlockInfoMap) - if (block.Value.Id == id) - bundles.Add(block.Key); - return bundles; - } - public static void GetDepBundles(ref List bundles) - { - for (int i = 0; i < bundles.Count; i++) - { - var bundle = bundles[i]; - bundles.AddRange(GetBundleDep(bundle)); - } - bundles = bundles.Distinct().ToList(); - } - public static bool CheckIsLegitAssetPath(ulong hash) - { - var asset = new Asset() { Hash = hash }; - return AssetLocationMap.ElementAtOrDefault(asset.Pre).ContainsKey(asset.Last); - } - } - public class BundleInfo - { - public int Bundle; - public string Path; - } - public class Asset - { - public ulong Hash; - public uint Last => (uint)(Hash >> 8); - public byte Pre => (byte)(Hash & 0xFF); - } - public class Block - { - public int Id; - public int Offset; - } - public class BlockFile - { - public int LanguageCode; - public int Id; } - public class AssetIndex + public record AssetIndex { public Dictionary Types { get; set; } - public class SubAssetInfo + public record SubAssetInfo { public string Name { get; set; } public byte PathHashPre { get; set; } @@ -184,7 +100,7 @@ namespace AssetStudio public Dictionary> Dependencies { get; set; } public List PreloadBlocks { get; set; } public List PreloadShaderBlocks { get; set; } - public class BlockInfo + public record BlockInfo { public byte Language { get; set; } public uint Id { get; set; } @@ -192,5 +108,16 @@ namespace AssetStudio } public Dictionary Assets { get; set; } public List SortList { get; set; } + + public AssetIndex() + { + Types = new Dictionary(); + SubAssets = new Dictionary>(); + Dependencies = new Dictionary>(); + PreloadBlocks = new List(); + PreloadShaderBlocks = new List(); + Assets = new Dictionary(); + SortList = new List(); + } } } diff --git a/AssetStudio/SerializedFile.cs b/AssetStudio/SerializedFile.cs index e3167e5..151d21e 100644 --- a/AssetStudio/SerializedFile.cs +++ b/AssetStudio/SerializedFile.cs @@ -387,6 +387,11 @@ namespace AssetStudio ObjectsDic.Add(obj.m_PathID, obj); } + public bool IsLoaded(ObjectInfo objInfo) + { + return ObjectsDic.TryGetValue(objInfo.m_PathID, out var obj) && obj.type == (ClassIDType)objInfo.classID; + } + private static int DecodeClassID(int value) { var bytes = BitConverter.GetBytes(value); diff --git a/AssetStudioCLI/App.config b/AssetStudioCLI/App.config index 7b3eab2..934689a 100644 --- a/AssetStudioCLI/App.config +++ b/AssetStudioCLI/App.config @@ -1,11 +1,8 @@  - - - + - @@ -14,6 +11,7 @@ + @@ -22,14 +20,5 @@ - - - - - - - - - \ No newline at end of file diff --git a/AssetStudioCLI/AssetStudioCLI.csproj b/AssetStudioCLI/AssetStudioCLI.csproj index a07a7d7..31a985e 100644 --- a/AssetStudioCLI/AssetStudioCLI.csproj +++ b/AssetStudioCLI/AssetStudioCLI.csproj @@ -22,7 +22,7 @@ - + True True diff --git a/AssetStudioCLI/Components/CommandLine.cs b/AssetStudioCLI/Components/CommandLine.cs new file mode 100644 index 0000000..43a1476 --- /dev/null +++ b/AssetStudioCLI/Components/CommandLine.cs @@ -0,0 +1,189 @@ +using System; +using System.IO; +using System.Linq; +using System.CommandLine; +using System.CommandLine.Binding; +using System.CommandLine.Parsing; +using System.Text.RegularExpressions; +using AssetStudio; + +namespace AssetStudioCLI +{ + public static class CommandLine + { + public static void Init(string[] args) + { + var rootCommand = RegisterOptions(); + rootCommand.Invoke(args); + } + public static RootCommand RegisterOptions() + { + var optionsBinder = new OptionsBinder(); + var rootCommand = new RootCommand() + { + optionsBinder.Silent, + optionsBinder.TypeFilter, + optionsBinder.NameFilter, + optionsBinder.ContainerFilter, + optionsBinder.GameName, + optionsBinder.MapOp, + optionsBinder.MapType, + optionsBinder.MapName, + optionsBinder.GroupAssetsType, + optionsBinder.Model, + optionsBinder.Key, + optionsBinder.AIFile, + optionsBinder.DummyDllFolder, + optionsBinder.Input, + optionsBinder.Output + }; + + rootCommand.SetHandler(Program.Run, optionsBinder); + + return rootCommand; + } + } + public class Options + { + public bool Silent { get; set; } + public ClassIDType[] TypeFilter { get; set; } + public Regex[] NameFilter { get; set; } + public Regex[] ContainerFilter { get; set; } + public string GameName { get; set; } + public MapOpType MapOp { get; set; } + public ExportListType MapType { get; set; } + public string MapName { get; set; } + public AssetGroupOption GroupAssetsType { get; set; } + public bool Model { get; set; } + public byte Key { get; set; } + public FileInfo AIFile { get; set; } + public DirectoryInfo DummyDllFolder { get; set; } + public FileInfo Input { get; set; } + public DirectoryInfo Output { get; set; } + } + + public class OptionsBinder : BinderBase + { + public readonly Option Silent; + public readonly Option TypeFilter; + public readonly Option NameFilter; + public readonly Option ContainerFilter; + public readonly Option GameName; + public readonly Option MapOp; + public readonly Option MapType; + public readonly Option MapName; + public readonly Option GroupAssetsType; + public readonly Option Model; + public readonly Option Key; + public readonly Option AIFile; + public readonly Option DummyDllFolder; + public readonly Argument Input; + public readonly Argument Output; + + + public OptionsBinder() + { + Silent = new Option("--silent", "Hide log messages."); + TypeFilter = new Option("--types", "Specify unity class type(s)") { AllowMultipleArgumentsPerToken = true, ArgumentHelpName = "Texture2D|Sprite|etc.." }; + NameFilter = new Option("--names", result => result.Tokens.Select(x => new Regex(x.Value, RegexOptions.IgnoreCase)).ToArray(), false, "Specify name regex filter(s).") { AllowMultipleArgumentsPerToken = true }; + ContainerFilter = new Option("--containers", result => result.Tokens.Select(x => new Regex(x.Value, RegexOptions.IgnoreCase)).ToArray(), false, "Specify container regex filter(s).") { AllowMultipleArgumentsPerToken = true }; + GameName = new Option("--game", $"Specify Game.") { IsRequired = true }; + MapOp = new Option("--map_op", "Specify which map to build."); + MapType = new Option("--map_type", "AssetMap output type."); + MapName = new Option("--map_name", () => "assets_map", "Specify AssetMap file name."); + GroupAssetsType = new Option("--group_assets", "Specify how exported assets should be grouped."); + Model = new Option("--models", "Enable to export models only"); + AIFile = new Option("--ai_file", "Specify asset_index json file path (to recover GI containers).").LegalFilePathsOnly(); + DummyDllFolder = new Option("--dummy_dlls", "Specify DummyDll path.").LegalFilePathsOnly(); + Input = new Argument("input_path", "Input file/folder.").LegalFilePathsOnly(); + Output = new Argument("output_path", "Output folder.").LegalFilePathsOnly(); + + Key = new Option("--key", result => + { + var value = result.Tokens.Single().Value; + if (value.StartsWith("0x")) + { + value = value[2..]; + return Convert.ToByte(value, 0x10); + } + else + { + return byte.Parse(value); + } + }, false, "XOR key to decrypt MiHoYoBinData."); + + TypeFilter.AddValidator(FilterValidator); + NameFilter.AddValidator(FilterValidator); + ContainerFilter.AddValidator(FilterValidator); + Key.AddValidator(result => + { + var value = result.Tokens.Single().Value; + try + { + if (value.StartsWith("0x")) + { + value = value.Substring(2); + Convert.ToByte(value, 0x10); + } + else + { + byte.Parse(value); + } + } + catch (Exception e) + { + result.ErrorMessage = "Invalid byte value.\n" + e.Message; + } + }); + + GameName.FromAmong(GameManager.GetGameNames()); + + GroupAssetsType.SetDefaultValue(AssetGroupOption.ByType); + MapOp.SetDefaultValue(MapOpType.Load); + MapType.SetDefaultValue(ExportListType.XML); + } + + public void FilterValidator(OptionResult result) + { + var values = result.Tokens.Select(x => x.Value).ToArray(); + foreach (var val in values) + { + if (string.IsNullOrWhiteSpace(val)) + { + result.ErrorMessage = "Empty string."; + return; + } + + try + { + Regex.Match("", val, RegexOptions.IgnoreCase); + } + catch (ArgumentException e) + { + result.ErrorMessage = "Invalid Regex.\n" + e.Message; + return; + } + } + } + + protected override Options GetBoundValue(BindingContext bindingContext) => + new() + { + Silent = bindingContext.ParseResult.GetValueForOption(Silent), + TypeFilter = bindingContext.ParseResult.GetValueForOption(TypeFilter), + NameFilter = bindingContext.ParseResult.GetValueForOption(NameFilter), + ContainerFilter = bindingContext.ParseResult.GetValueForOption(ContainerFilter), + GameName = bindingContext.ParseResult.GetValueForOption(GameName), + MapOp = bindingContext.ParseResult.GetValueForOption(MapOp), + MapType = bindingContext.ParseResult.GetValueForOption(MapType), + MapName = bindingContext.ParseResult.GetValueForOption(MapName), + GroupAssetsType = bindingContext.ParseResult.GetValueForOption(GroupAssetsType), + Model = bindingContext.ParseResult.GetValueForOption(Model), + Key = bindingContext.ParseResult.GetValueForOption(Key), + AIFile = bindingContext.ParseResult.GetValueForOption(AIFile), + DummyDllFolder = bindingContext.ParseResult.GetValueForOption(DummyDllFolder), + Input = bindingContext.ParseResult.GetValueForArgument(Input), + Output = bindingContext.ParseResult.GetValueForArgument(Output) + }; + } +} diff --git a/AssetStudioCLI/Exporter.cs b/AssetStudioCLI/Exporter.cs index 3be7bea..8a912a8 100644 --- a/AssetStudioCLI/Exporter.cs +++ b/AssetStudioCLI/Exporter.cs @@ -91,6 +91,22 @@ namespace AssetStudioCLI return true; } + public static bool ExportMonoBehaviour(AssetItem item, string exportPath) + { + if (!TryExportFile(exportPath, item, ".json", out var exportFullPath)) + return false; + var m_MonoBehaviour = (MonoBehaviour)item.Asset; + var type = m_MonoBehaviour.ToType(); + if (type == null) + { + var m_Type = Studio.MonoBehaviourToTypeTree(m_MonoBehaviour); + type = m_MonoBehaviour.ToType(m_Type); + } + var str = JsonConvert.SerializeObject(type, Formatting.Indented); + File.WriteAllText(exportFullPath, str); + return true; + } + public static bool ExportMiHoYoBinData(AssetItem item, string exportPath) { string exportFullPath; @@ -315,6 +331,13 @@ namespace AssetStudioCLI return true; } + public static bool ExportGameObject(AssetItem item, string exportPath, List animationList = null) + { + var m_GameObject = (GameObject)item.Asset; + ExportGameObject(m_GameObject, exportPath, animationList); + return true; + } + public static void ExportGameObject(GameObject gameObject, string exportPath, List animationList = null) { var convert = animationList != null @@ -369,6 +392,8 @@ namespace AssetStudioCLI { switch (item.Type) { + case ClassIDType.GameObject: + return ExportGameObject(item, exportPath); case ClassIDType.Texture2D: return ExportTexture2D(item, exportPath); case ClassIDType.AudioClip: @@ -378,7 +403,7 @@ namespace AssetStudioCLI case ClassIDType.TextAsset: return ExportTextAsset(item, exportPath); case ClassIDType.MonoBehaviour: - return false; + return ExportMonoBehaviour(item, exportPath); case ClassIDType.Font: return ExportFont(item, exportPath); case ClassIDType.Mesh: @@ -390,7 +415,7 @@ namespace AssetStudioCLI case ClassIDType.Sprite: return ExportSprite(item, exportPath); case ClassIDType.Animator: - return ExportAnimator(item, exportPath, new List()); + return ExportAnimator(item, exportPath); case ClassIDType.AnimationClip: return false; case ClassIDType.MiHoYoBinData: diff --git a/AssetStudioCLI/Program.cs b/AssetStudioCLI/Program.cs index 07f29b1..4db5a27 100644 --- a/AssetStudioCLI/Program.cs +++ b/AssetStudioCLI/Program.cs @@ -1,276 +1,132 @@ using System; using System.IO; using System.Linq; -using System.CommandLine; -using System.CommandLine.Binding; -using System.Text.RegularExpressions; -using static AssetStudioCLI.Studio; +using System.Threading; using AssetStudio; -using System.CommandLine.Parsing; +using static AssetStudioCLI.Studio; namespace AssetStudioCLI { public class Program { - public static void Main(string[] args) + public static void Main(string[] args) => CommandLine.Init(args); + + public static void Run(Options o) { - var rootCommand = RegisterOptions(); - rootCommand.Invoke(args); - } - - public static RootCommand RegisterOptions() - { - var optionsBinder = new OptionsBinder(); - var rootCommand = new RootCommand() + try { - optionsBinder.Silent, - optionsBinder.TypeFilter, - optionsBinder.NameFilter, - optionsBinder.ContainerFilter, - optionsBinder.GameName, - optionsBinder.MapOp, - optionsBinder.MapType, - optionsBinder.MapName, - optionsBinder.GroupAssetsType, - optionsBinder.IncludeAnimators, - optionsBinder.SkipMiHoYoBinData, - optionsBinder.Key, - optionsBinder.AIFile, - optionsBinder.Input, - optionsBinder.Output - }; + var game = GameManager.GetGame(o.GameName); - rootCommand.SetHandler((Options o) => - { - try + if (game == null) { - var game = GameManager.GetGame(o.GameName); - - if (game == null) - { - Console.WriteLine("Invalid Game !!"); - Console.WriteLine(GameManager.SupportedGames()); - return; - } - - Studio.Game = game; - Logger.Default = new ConsoleLogger(); - assetsManager.Silent = o.Silent; - assetsManager.Game = game; - MiHoYoBinData.Exportable = !o.SkipMiHoYoBinData; - - if (o.Key != default) - { - if (o.SkipMiHoYoBinData) - { - Logger.Warning("Key is set but IndexObject/MiHoYoBinData is excluded, ignoring key..."); - } - else - { - MiHoYoBinData.Encrypted = true; - MiHoYoBinData.Key = o.Key; - } - } - - if (o.AIFile != null && game.Type.IsGISubGroup()) - { - ResourceIndex.FromFile(o.AIFile.FullName); - } - - Logger.Info("Scanning for files"); - var files = o.Input.Attributes.HasFlag(FileAttributes.Directory) ? Directory.GetFiles(o.Input.FullName, "*.*", SearchOption.AllDirectories).OrderBy(x => x.Length).ToArray() : new string[] { o.Input.FullName }; - Logger.Info(string.Format("Found {0} file(s)", files.Length)); - - if (o.MapOp.Equals(MapOpType.None)) - { - var i = 0; - foreach (var file in files) - { - assetsManager.LoadFiles(file); - if (assetsManager.assetsFileList.Count > 0) - { - BuildAssetData(o.TypeFilter, o.NameFilter, o.ContainerFilter, ref i); - ExportAssets(o.Output.FullName, exportableAssets, o.GroupAssetsType); - } - exportableAssets.Clear(); - assetsManager.Clear(); - } - } - if (o.MapOp.HasFlag(MapOpType.CABMap)) - { - AssetsHelper.BuildCABMap(files, "", "", game); - } - if (o.MapOp.HasFlag(MapOpType.AssetMap)) - { - if (files.Length == 1) - { - throw new Exception("Unable to build AssetMap with input_path as a file !!"); - } - var assets = AssetsHelper.BuildAssetMap(files, game, o.TypeFilter, o.NameFilter, o.ContainerFilter); - if (!o.Output.Exists) - { - o.Output.Create(); - } - if (string.IsNullOrEmpty(o.MapName)) - { - o.MapName = "assets_map"; - } - AssetsHelper.ExportAssetsMap(assets, o.MapName, o.Output.FullName, o.MapType); - } + Console.WriteLine("Invalid Game !!"); + Console.WriteLine(GameManager.SupportedGames()); + return; } - catch (Exception e) + + Studio.Game = game; + Logger.Default = new ConsoleLogger(); + assetsManager.Silent = o.Silent; + assetsManager.Game = game; + + + if (!o.TypeFilter.IsNullOrEmpty()) { - Console.WriteLine(e.Message); - Console.WriteLine(e.StackTrace); - } - }, optionsBinder); - - return rootCommand; - } - } - - public class Options - { - public bool Silent { get; set; } - public ClassIDType[] TypeFilter { get; set; } - public Regex[] NameFilter { get; set; } - public Regex[] ContainerFilter { get; set; } - public string GameName { get; set; } - public MapOpType MapOp { get; set; } - public ExportListType MapType { get; set; } - public string MapName { get; set; } - public AssetGroupOption GroupAssetsType { get; set; } - public bool SkipRenderer { get; set; } - public bool SkipMiHoYoBinData { get; set; } - public byte Key { get; set; } - public FileInfo AIFile { get; set; } - public FileInfo Input { get; set; } - public DirectoryInfo Output { get; set; } - } - - public class OptionsBinder : BinderBase - { - public readonly Option Silent; - public readonly Option TypeFilter; - public readonly Option NameFilter; - public readonly Option ContainerFilter; - public readonly Option GameName; - public readonly Option MapOp; - public readonly Option MapType; - public readonly Option MapName; - public readonly Option GroupAssetsType; - public readonly Option IncludeAnimators; - public readonly Option SkipMiHoYoBinData; - public readonly Option Key; - public readonly Option AIFile; - public readonly Argument Input; - public readonly Argument Output; - - public OptionsBinder() - { - Silent = new Option("--silent", "Hide log messages."); - TypeFilter = new Option("--types", "Specify unity class type(s)") { AllowMultipleArgumentsPerToken = true, ArgumentHelpName = "Texture2D|Sprite|etc.." }; - NameFilter = new Option("--names", result => result.Tokens.Select(x => new Regex(x.Value, RegexOptions.IgnoreCase)).ToArray(), false, "Specify name regex filter(s).") { AllowMultipleArgumentsPerToken = true }; - ContainerFilter = new Option("--containers", result => result.Tokens.Select(x => new Regex(x.Value, RegexOptions.IgnoreCase)).ToArray(), false, "Specify container regex filter(s).") { AllowMultipleArgumentsPerToken = true }; - GameName = new Option("--game", $"Specify Game.") { IsRequired = true }; - MapOp = new Option("--map_op", "Specify which map to build."); - MapType = new Option("--map_type", "AssetMap output type."); - MapName = new Option("--map_name", "Specify AssetMap file name."); - GroupAssetsType = new Option("--group_assets_type", "Specify how exported assets should be grouped."); - IncludeAnimators = new Option("--skip_re", "Include Animator with Export."); - SkipMiHoYoBinData = new Option("--skip_mihoyobindata", "Exclude IndexObject/MiHoYoBinData from AssetMap/Export."); - AIFile = new Option("--ai_file", "Specify asset_index json file path (to recover GI containers).").LegalFilePathsOnly(); - Input = new Argument("input_path", "Input file/folder.").LegalFilePathsOnly(); - Output = new Argument("output_path", "Output folder.").LegalFilePathsOnly(); - - Key = new Option("--key", result => - { - var value = result.Tokens.Single().Value; - if (value.StartsWith("0x")) - { - value = value.Substring(2); - return Convert.ToByte(value, 0x10); - } - else - { - return byte.Parse(value); - } - }, false, "Key to decrypt MiHoYoBinData."); - - TypeFilter.AddValidator(FilterValidator); - NameFilter.AddValidator(FilterValidator); - ContainerFilter.AddValidator(FilterValidator); - Key.AddValidator(result => - { - var value = result.Tokens.Single().Value; - try - { - if (value.StartsWith("0x")) + foreach (var kv in assetsManager.ExportableTypes) { - value = value.Substring(2); - Convert.ToByte(value, 0x10); + assetsManager.ExportableTypes[kv.Key] = o.TypeFilter.Contains(kv.Key); + } + + } + + if (o.Model) + { + foreach (var kv in assetsManager.ExportableTypes) + { + assetsManager.ExportableTypes[kv.Key] = false; + } + + assetsManager.ExportableTypes[ClassIDType.Animator] = true; + assetsManager.ExportableTypes[ClassIDType.GameObject] = true; + assetsManager.ExportableTypes[ClassIDType.Texture2D] = true; + assetsManager.ExportableTypes[ClassIDType.Material] = true; + assetsManager.ExportableTypes[ClassIDType.Renderer] = true; + assetsManager.ExportableTypes[ClassIDType.Mesh] = true; + + ModelOnly = true; + } + + if (o.Key != default) + { + if (!assetsManager.ExportableTypes[ClassIDType.MiHoYoBinData]) + { + Logger.Warning("Key is set but MiHoYoBinData is skipped, ignoring key..."); } else { - byte.Parse(value); + MiHoYoBinData.Encrypted = true; + MiHoYoBinData.Key = o.Key; } } - catch(Exception e) + + if (o.AIFile != null && game.Type.IsGISubGroup()) { - result.ErrorMessage = "Invalid byte value.\n" + e.Message; - } - }); + ResourceIndex.FromFile(o.AIFile.FullName); + } - GameName.FromAmong(GameManager.GetGameNames()); - - GroupAssetsType.SetDefaultValue(0); - MapOp.SetDefaultValue(MapOpType.None); - MapType.SetDefaultValue(ExportListType.XML); - } - - public void FilterValidator(OptionResult result) - { - { - var values = result.Tokens.Select(x => x.Value).ToArray(); - foreach (var val in values) + if (o.DummyDllFolder != null) { - if (string.IsNullOrWhiteSpace(val)) - { - result.ErrorMessage = "Empty string."; - return; - } + assemblyLoader.Load(o.DummyDllFolder.FullName); + } - try + Logger.Info("Scanning for files"); + var files = o.Input.Attributes.HasFlag(FileAttributes.Directory) ? Directory.GetFiles(o.Input.FullName, "*.*", SearchOption.AllDirectories).OrderBy(x => x.Length).ToArray() : new string[] { o.Input.FullName }; + Logger.Info(string.Format("Found {0} file(s)", files.Length)); + + if (o.MapOp.HasFlag(MapOpType.Build)) + { + AssetsHelper.BuildMap(files, o.MapName, o.Input.FullName, game); + } + if (o.MapOp.HasFlag(MapOpType.Load)) + { + AssetsHelper.LoadMap(o.MapName); + assetsManager.ResolveDependencies = true; + } + if (o.MapOp.HasFlag(MapOpType.List)) + { + if (files.Length == 1) { - Regex.Match("", val, RegexOptions.IgnoreCase); + throw new Exception("Unable to build AssetMap with input_path as a file !!"); } - catch (ArgumentException e) + var assets = AssetsHelper.BuildAssetMap(files, game, o.NameFilter, o.ContainerFilter); + if (!o.Output.Exists) { - result.ErrorMessage = "Invalid Regex.\n" + e.Message; - return; + o.Output.Create(); + } + var resetEvent = new ManualResetEvent(false); + AssetsHelper.ExportAssetsMap(assets, o.MapName, o.Output.FullName, o.MapType, resetEvent); + resetEvent.WaitOne(); + } + if (o.MapOp.Equals(MapOpType.None) || o.MapOp.HasFlag(MapOpType.Load)) + { + var i = 0; + foreach (var file in files) + { + assetsManager.LoadFiles(file); + if (assetsManager.assetsFileList.Count > 0) + { + BuildAssetData(o.NameFilter, o.ContainerFilter, ref i); + ExportAssets(o.Output.FullName, exportableAssets, o.GroupAssetsType); + } + exportableAssets.Clear(); + assetsManager.Clear(); } } } + catch (Exception e) + { + Console.WriteLine(e); + } } - - protected override Options GetBoundValue(BindingContext bindingContext) => - new Options - { - Silent = bindingContext.ParseResult.GetValueForOption(Silent), - TypeFilter = bindingContext.ParseResult.GetValueForOption(TypeFilter), - NameFilter = bindingContext.ParseResult.GetValueForOption(NameFilter), - ContainerFilter = bindingContext.ParseResult.GetValueForOption(ContainerFilter), - GameName = bindingContext.ParseResult.GetValueForOption(GameName), - MapOp = bindingContext.ParseResult.GetValueForOption(MapOp), - MapType = bindingContext.ParseResult.GetValueForOption(MapType), - MapName = bindingContext.ParseResult.GetValueForOption(MapName), - GroupAssetsType = bindingContext.ParseResult.GetValueForOption(GroupAssetsType), - SkipRenderer = bindingContext.ParseResult.GetValueForOption(IncludeAnimators), - SkipMiHoYoBinData = bindingContext.ParseResult.GetValueForOption(SkipMiHoYoBinData), - Key = bindingContext.ParseResult.GetValueForOption(Key), - AIFile = bindingContext.ParseResult.GetValueForOption(AIFile), - Input = bindingContext.ParseResult.GetValueForArgument(Input), - Output = bindingContext.ParseResult.GetValueForArgument(Output) - }; } } \ No newline at end of file diff --git a/AssetStudioCLI/Options.cs b/AssetStudioCLI/Settings.cs similarity index 96% rename from AssetStudioCLI/Options.cs rename to AssetStudioCLI/Settings.cs index 6a6c0b0..55aa057 100644 --- a/AssetStudioCLI/Options.cs +++ b/AssetStudioCLI/Settings.cs @@ -24,6 +24,7 @@ namespace AssetStudioCLI.Properties { } catch (Exception) { + Console.WriteLine($"Invalid value at \"{key}\", switching to default value [{defaultValue}] !!"); return defaultValue; } diff --git a/AssetStudioCLI/Studio.cs b/AssetStudioCLI/Studio.cs index aa8dd7b..0318c48 100644 --- a/AssetStudioCLI/Studio.cs +++ b/AssetStudioCLI/Studio.cs @@ -10,6 +10,7 @@ using System.Text.RegularExpressions; using static AssetStudioCLI.Exporter; using Object = AssetStudio.Object; using System.Globalization; +using System.Xml; namespace AssetStudioCLI { @@ -17,9 +18,11 @@ namespace AssetStudioCLI public enum MapOpType { None, - AssetMap, - CABMap, - Both + Load, + Build, + Both, + List, + All = Build | Load | List } public enum AssetGroupOption @@ -32,9 +35,12 @@ namespace AssetStudioCLI internal static class Studio { - public static AssetsManager assetsManager = new AssetsManager() { ResolveDependencies = false }; - public static List exportableAssets = new List(); public static Game Game; + public static bool ModelOnly = false; + public static bool SkipContainer = false; + public static AssetsManager assetsManager = new AssetsManager() { ResolveDependencies = false }; + public static AssemblyLoader assemblyLoader = new AssemblyLoader(); + public static List exportableAssets = new List(); public static int ExtractFolder(string path, string savePath) { @@ -213,13 +219,17 @@ namespace AssetStudioCLI if (int.TryParse(asset.Container, out var value)) { var last = unchecked((uint)value); - var path = ResourceIndex.GetAssetPath(last); - if (!string.IsNullOrEmpty(path)) + var name = Path.GetFileNameWithoutExtension(asset.SourceFile.originalPath); + if (uint.TryParse(name, out var id)) { - asset.Container = path; - if (asset.Type == ClassIDType.MiHoYoBinData) + var path = ResourceIndex.GetContainer(id, last); + if (!string.IsNullOrEmpty(path)) { - asset.Text = Path.GetFileNameWithoutExtension(path); + asset.Container = path; + if (asset.Type == ClassIDType.MiHoYoBinData) + { + asset.Text = Path.GetFileNameWithoutExtension(path); + } } } } @@ -228,121 +238,16 @@ namespace AssetStudioCLI } } - public static void BuildAssetData(ClassIDType[] typeFilters, Regex[] nameFilters, Regex[] containerFilters, ref int i) + public static void BuildAssetData(Regex[] nameFilters, Regex[] containerFilters, ref int i) { - string productName = null; - var objectCount = assetsManager.assetsFileList.Sum(x => x.Objects.Count); - var objectAssetItemDic = new Dictionary(objectCount); + var objectAssetItemDic = new Dictionary(); var mihoyoBinDataNames = new List<(PPtr, string)>(); var containers = new List<(PPtr, string)>(); foreach (var assetsFile in assetsManager.assetsFileList) { foreach (var asset in assetsFile.Objects) { - var assetItem = new AssetItem(asset); - objectAssetItemDic.Add(asset, assetItem); - assetItem.UniqueID = "#" + i++; - assetItem.Text = ""; - 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; - 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; - 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 _: - case Material _: - 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; - } - 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 IndexObject m_IndexObject: - foreach (var index in m_IndexObject.AssetMap) - { - mihoyoBinDataNames.Add((index.Value.Object, index.Key)); - } - assetItem.Text = "IndexObject"; - break; - case MiHoYoBinData m_MiHoYoBinData: - exportable = MiHoYoBinData.Exportable; - 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; - exportable = true; - break; - } - if (assetItem.Text == "") - { - assetItem.Text = assetItem.TypeString + assetItem.UniqueID; - } - var isMatchRegex = nameFilters.Length == 0 || nameFilters.Any(x => x.IsMatch(assetItem.Text)); - var isFilteredType = typeFilters.Length == 0 || typeFilters.Contains(assetItem.Asset.type); - if (isMatchRegex && isFilteredType && exportable) - { - exportableAssets.Add(assetItem); - } + ProcessAssetData(asset, nameFilters, objectAssetItemDic, mihoyoBinDataNames, containers, ref i); } } foreach ((var pptr, var name) in mihoyoBinDataNames) @@ -358,26 +263,138 @@ namespace AssetStudioCLI else assetItem.Text = $"BinFile #{assetItem.m_PathID}"; } } - foreach ((var pptr, var container) in containers) + if (!SkipContainer) { - if (pptr.TryGet(out var obj)) + foreach ((var pptr, var container) in containers) { - var item = objectAssetItemDic[obj]; - if (containerFilters.IsNullOrEmpty() || containerFilters.Any(x => x.IsMatch(container))) + if (pptr.TryGet(out var obj)) { - item.Container = container; + if (!objectAssetItemDic.TryGetValue(obj, out var item)) + { + ProcessAssetData(obj, nameFilters, objectAssetItemDic, mihoyoBinDataNames, containers, ref i); + item = objectAssetItemDic[obj]; + } + if (containerFilters.IsNullOrEmpty() || containerFilters.Any(x => x.IsMatch(container))) + { + item.Container = container; + } + else + { + exportableAssets.Remove(item); + } + } + } + containers.Clear(); + if (Game.Type.IsGISubGroup()) + { + UpdateContainers(); + } + } + } + + public static void ProcessAssetData(Object asset, Regex[] nameFilters, Dictionary objectAssetItemDic, List<(PPtr, string)> mihoyoBinDataNames, List<(PPtr, string)> containers, ref int i) + { + 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; + exportable = ModelOnly && m_GameObject.HasModel(); + 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 = !ModelOnly; + 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 = !ModelOnly; + 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 = !ModelOnly; + break; + case Shader m_Shader: + assetItem.Text = m_Shader.m_ParsedForm?.m_Name ?? m_Shader.m_Name; + exportable = !ModelOnly; + break; + case Mesh _: + case TextAsset _: + case AnimationClip _: + case Font _: + case Sprite _: + case Material _: + assetItem.Text = ((NamedObject)asset).m_Name; + exportable = !ModelOnly; + break; + case Animator m_Animator: + if (m_Animator.m_GameObject.TryGet(out var gameObject)) + { + assetItem.Text = gameObject.m_Name; + } + exportable = !ModelOnly; + 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 { - exportableAssets.Remove(item); + assetItem.Text = m_MonoBehaviour.m_Name; } - } + exportable = !ModelOnly; + 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 IndexObject m_IndexObject: + foreach (var index in m_IndexObject.AssetMap) + { + mihoyoBinDataNames.Add((index.Value.Object, index.Key)); + } + assetItem.Text = "IndexObject"; + break; + case MiHoYoBinData m_MiHoYoBinData: + exportable = !ModelOnly; + 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 (Game.Type.IsGISubGroup()) + if (assetItem.Text == "") { - UpdateContainers(); + assetItem.Text = assetItem.TypeString + assetItem.UniqueID; + } + var isMatchRegex = nameFilters.Length == 0 || nameFilters.Any(x => x.IsMatch(assetItem.Text)); + if (isMatchRegex && exportable) + { + exportableAssets.Add(assetItem); } - containers.Clear(); } public static void ExportAssets(string savePath, List toExportAssets, AssetGroupOption assetGroupOption) @@ -448,28 +465,35 @@ namespace AssetStudioCLI { case ExportListType.XML: filename = Path.Combine(savePath, $"{exportListName}.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.Name), - new XElement("Container", asset.Container), - new XElement("Type", new XAttribute("id", (int)asset.Type), asset.Type.ToString()), - new XElement("PathID", asset.PathID), - new XElement("Source", asset.Source) - ) - ) - ) - ); - doc.Save(filename); + var settings = new XmlWriterSettings() { Indent = true }; + using (XmlWriter writer = XmlWriter.Create(filename, settings)) + { + writer.WriteStartDocument(); + writer.WriteStartElement("Assets"); + writer.WriteAttributeString("filename", filename); + writer.WriteAttributeString("createdAt", DateTime.UtcNow.ToString("s")); + foreach (var asset in toExportAssets) + { + writer.WriteStartElement("Asset"); + writer.WriteElementString("Name", asset.Name); + writer.WriteElementString("Container", asset.Container); + writer.WriteStartElement("Type"); + writer.WriteAttributeString("id", ((int)asset.Type).ToString()); + writer.WriteValue(asset.Type.ToString()); + writer.WriteEndElement(); + writer.WriteElementString("PathID", asset.PathID.ToString()); + writer.WriteElementString("Source", asset.Source); + writer.WriteEndElement(); + } + writer.WriteEndElement(); + writer.WriteEndDocument(); + } break; case ExportListType.JSON: filename = Path.Combine(savePath, $"{exportListName}.json"); using (StreamWriter file = File.CreateText(filename)) { - JsonSerializer serializer = new JsonSerializer() { Formatting = Formatting.Indented }; + JsonSerializer serializer = new JsonSerializer() { Formatting = Newtonsoft.Json.Formatting.Indented }; serializer.Converters.Add(new StringEnumConverter()); serializer.Serialize(file, toExportAssets); } @@ -482,5 +506,10 @@ namespace AssetStudioCLI Logger.Info($"AssetMap build successfully !!"); } + + public static TypeTree MonoBehaviourToTypeTree(MonoBehaviour m_MonoBehaviour) + { + return m_MonoBehaviour.ConvertToTypeTree(assemblyLoader); + } } } diff --git a/AssetStudioGUI/App.config b/AssetStudioGUI/App.config index 29065e4..d8c2e20 100644 --- a/AssetStudioGUI/App.config +++ b/AssetStudioGUI/App.config @@ -85,9 +85,6 @@ 0 - - True - True diff --git a/AssetStudioGUI/AssetStudioGUIForm.Designer.cs b/AssetStudioGUI/AssetStudioGUIForm.Designer.cs index a84ac8e..6c9d998 100644 --- a/AssetStudioGUI/AssetStudioGUIForm.Designer.cs +++ b/AssetStudioGUI/AssetStudioGUIForm.Designer.cs @@ -93,8 +93,8 @@ namespace AssetStudioGUI clearConsoleToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); miscToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); assetHelpersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - cABMapToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - CABMapNameComboBox = new System.Windows.Forms.ToolStripComboBox(); + MapToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + MapNameComboBox = new System.Windows.Forms.ToolStripComboBox(); toolStripMenuItem20 = new System.Windows.Forms.ToolStripMenuItem(); toolStripMenuItem21 = new System.Windows.Forms.ToolStripMenuItem(); assetMapToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); @@ -604,37 +604,37 @@ namespace AssetStudioGUI // // assetHelpersToolStripMenuItem // - assetHelpersToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { cABMapToolStripMenuItem, assetMapToolStripMenuItem }); + assetHelpersToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { MapToolStripMenuItem, assetMapToolStripMenuItem }); assetHelpersToolStripMenuItem.Name = "assetHelpersToolStripMenuItem"; assetHelpersToolStripMenuItem.Size = new System.Drawing.Size(145, 22); assetHelpersToolStripMenuItem.Text = "Asset Helpers"; assetHelpersToolStripMenuItem.DropDownOpening += assetHelpersToolStripMenuItem_DropDownOpening; // - // cABMapToolStripMenuItem + // MapToolStripMenuItem // - cABMapToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { CABMapNameComboBox, toolStripMenuItem20, toolStripMenuItem21 }); - cABMapToolStripMenuItem.Name = "cABMapToolStripMenuItem"; - cABMapToolStripMenuItem.Size = new System.Drawing.Size(126, 22); - cABMapToolStripMenuItem.Text = "CABMap"; + MapToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { MapNameComboBox, toolStripMenuItem20, toolStripMenuItem21 }); + MapToolStripMenuItem.Name = "MapToolStripMenuItem"; + MapToolStripMenuItem.Size = new System.Drawing.Size(126, 22); + MapToolStripMenuItem.Text = "Map"; // - // CABMapNameComboBox + // MapNameComboBox // - CABMapNameComboBox.Name = "CABMapNameComboBox"; - CABMapNameComboBox.Size = new System.Drawing.Size(121, 23); - CABMapNameComboBox.ToolTipText = "Enter name of CABMap here"; + MapNameComboBox.Name = "MapNameComboBox"; + MapNameComboBox.Size = new System.Drawing.Size(121, 23); + MapNameComboBox.ToolTipText = "Enter name of Map here"; // // toolStripMenuItem20 // toolStripMenuItem20.Name = "toolStripMenuItem20"; toolStripMenuItem20.Size = new System.Drawing.Size(181, 22); - toolStripMenuItem20.Text = "Build CABMap"; + toolStripMenuItem20.Text = "Build Map"; toolStripMenuItem20.Click += toolStripMenuItem20_Click; // // toolStripMenuItem21 // toolStripMenuItem21.Name = "toolStripMenuItem21"; toolStripMenuItem21.Size = new System.Drawing.Size(181, 22); - toolStripMenuItem21.Text = "Clear CABMap"; + toolStripMenuItem21.Text = "Clear Map"; toolStripMenuItem21.Click += toolStripMenuItem21_Click; // // assetMapToolStripMenuItem @@ -1362,13 +1362,13 @@ namespace AssetStudioGUI private System.Windows.Forms.ToolStripMenuItem assetHelpersToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem toolStripMenuItem20; private System.Windows.Forms.ToolStripMenuItem toolStripMenuItem22; - private System.Windows.Forms.ToolStripComboBox CABMapNameComboBox; + private System.Windows.Forms.ToolStripComboBox MapNameComboBox; private System.Windows.Forms.ToolStripSeparator toolStripSeparator5; private System.Windows.Forms.ToolStripSeparator toolStripSeparator6; private System.Windows.Forms.ToolStripMenuItem resetToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem abortStripMenuItem; private System.Windows.Forms.ToolStripMenuItem toolStripMenuItem21; - private System.Windows.Forms.ToolStripMenuItem cABMapToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem MapToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem assetMapToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem loadAIToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem clearConsoleToolStripMenuItem; diff --git a/AssetStudioGUI/AssetStudioGUIForm.cs b/AssetStudioGUI/AssetStudioGUIForm.cs index 8cb31fa..9c55efa 100644 --- a/AssetStudioGUI/AssetStudioGUIForm.cs +++ b/AssetStudioGUI/AssetStudioGUIForm.cs @@ -108,7 +108,6 @@ namespace AssetStudioGUI displayInfo.Checked = Properties.Settings.Default.displayInfo; enablePreview.Checked = Properties.Settings.Default.enablePreview; assetsManager.ResolveDependencies = Properties.Settings.Default.enableResolveDependencies; - MiHoYoBinData.Exportable = Properties.Settings.Default.exportMiHoYoBinData; MiHoYoBinData.Encrypted = Properties.Settings.Default.encrypted; MiHoYoBinData.Key = Properties.Settings.Default.key; @@ -155,7 +154,7 @@ namespace AssetStudioGUI Studio.Game = GameManager.GetGame(Properties.Settings.Default.selectedGame); Logger.Info($"Target Game type is {Studio.Game.Type}"); - CABMapNameComboBox.SelectedIndexChanged += new EventHandler(specifyNameComboBox_SelectedIndexChanged); + MapNameComboBox.SelectedIndexChanged += new EventHandler(specifyNameComboBox_SelectedIndexChanged); } private void AssetStudioGUIForm_DragEnter(object sender, DragEventArgs e) { @@ -1719,8 +1718,8 @@ namespace AssetStudioGUI { if (assetHelpersToolStripMenuItem.Enabled) { - CABMapNameComboBox.Items.Clear(); - CABMapNameComboBox.Items.AddRange(AssetsHelper.GetMaps()); + MapNameComboBox.Items.Clear(); + MapNameComboBox.Items.AddRange(AssetsHelper.GetMaps()); } } @@ -1780,14 +1779,18 @@ namespace AssetStudioGUI if (int.TryParse(asset.Container, out var value)) { var last = unchecked((uint)value); - var path = ResourceIndex.GetAssetPath(last); - if (!string.IsNullOrEmpty(path)) + var name = Path.GetFileNameWithoutExtension(asset.SourceFile.originalPath); + if (uint.TryParse(name, out var id)) { - asset.Container = path; - asset.SubItems[1].Text = path; - if (asset.Type == ClassIDType.MiHoYoBinData) + var path = ResourceIndex.GetContainer(id, last); + if (!string.IsNullOrEmpty(path)) { - asset.Text = Path.GetFileNameWithoutExtension(path); + asset.Container = path; + asset.SubItems[1].Text = path; + if (asset.Type == ClassIDType.MiHoYoBinData) + { + asset.Text = Path.GetFileNameWithoutExtension(path); + } } } } @@ -1841,7 +1844,7 @@ namespace AssetStudioGUI miscToolStripMenuItem.DropDown.Visible = false; InvokeUpdate(miscToolStripMenuItem, false); - var name = CABMapNameComboBox.SelectedItem.ToString(); + var name = MapNameComboBox.SelectedItem.ToString(); await Task.Run(() => AssetsHelper.LoadMap(name)); ResetForm(); @@ -1856,8 +1859,8 @@ namespace AssetStudioGUI miscToolStripMenuItem.DropDown.Visible = false; InvokeUpdate(miscToolStripMenuItem, false); - var input = CABMapNameComboBox.Text; - var selectedText = CABMapNameComboBox.SelectedText; + var input = MapNameComboBox.Text; + var selectedText = MapNameComboBox.SelectedText; var name = ""; if (!string.IsNullOrEmpty(selectedText)) @@ -1877,14 +1880,14 @@ namespace AssetStudioGUI } else { - Logger.Error("CABMap name is empty, please enter any name in ComboBox above"); + Logger.Error("Map name is empty, please enter any name in ComboBox above"); InvokeUpdate(miscToolStripMenuItem, true); return; } - if (File.Exists(Path.Combine(AssetsHelper.CABMapName, $"{name}.bin"))) + if (File.Exists(Path.Combine(AssetsHelper.MapName, $"{name}.bin"))) { - var acceptOverride = MessageBox.Show("CABMap already exist, Do you want to override it ?", "Warning", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); + var acceptOverride = MessageBox.Show("Map already exist, Do you want to override it ?", "Warning", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (acceptOverride != DialogResult.Yes) { InvokeUpdate(miscToolStripMenuItem, true); @@ -1899,7 +1902,7 @@ namespace AssetStudioGUI Logger.Info("Scanning for files"); var files = Directory.GetFiles(openFolderDialog.Folder, "*.*", SearchOption.AllDirectories).ToArray(); Logger.Info($"Found {files.Length} files"); - await Task.Run(() => AssetsHelper.BuildCABMap(files, name, openFolderDialog.Folder, Studio.Game)); + await Task.Run(() => AssetsHelper.BuildMap(files, name, openFolderDialog.Folder, Studio.Game)); } InvokeUpdate(miscToolStripMenuItem, true); } @@ -1909,22 +1912,22 @@ namespace AssetStudioGUI miscToolStripMenuItem.DropDown.Visible = false; InvokeUpdate(miscToolStripMenuItem, false); - var acceptDelete = MessageBox.Show("CABMap will be deleted, this can't be undone, continue ?", "Warning", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); + var acceptDelete = MessageBox.Show("Map will be deleted, this can't be undone, continue ?", "Warning", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (acceptDelete != DialogResult.Yes) { InvokeUpdate(miscToolStripMenuItem, true); return; } - var name = CABMapNameComboBox.Text.ToString(); - var path = Path.Combine(AssetsHelper.CABMapName, $"{name}.bin"); + var name = MapNameComboBox.Text.ToString(); + var path = Path.Combine(AssetsHelper.MapName, $"{name}.bin"); if (File.Exists(path)) { File.Delete(path); Logger.Info($"{name} deleted successfully !!"); - CABMapNameComboBox.SelectedIndexChanged -= new EventHandler(specifyNameComboBox_SelectedIndexChanged); - CABMapNameComboBox.SelectedIndex = 0; - CABMapNameComboBox.SelectedIndexChanged += new EventHandler(specifyNameComboBox_SelectedIndexChanged); + MapNameComboBox.SelectedIndexChanged -= new EventHandler(specifyNameComboBox_SelectedIndexChanged); + MapNameComboBox.SelectedIndex = 0; + MapNameComboBox.SelectedIndexChanged += new EventHandler(specifyNameComboBox_SelectedIndexChanged); } InvokeUpdate(miscToolStripMenuItem, true); diff --git a/AssetStudioGUI/ExportOptions.Designer.cs b/AssetStudioGUI/ExportOptions.Designer.cs index f6850df..4f89738 100644 --- a/AssetStudioGUI/ExportOptions.Designer.cs +++ b/AssetStudioGUI/ExportOptions.Designer.cs @@ -557,9 +557,9 @@ namespace AssetStudioGUI label7.AutoSize = true; label7.Location = new System.Drawing.Point(7, 94); label7.Name = "label7"; - label7.Size = new System.Drawing.Size(91, 15); + label7.Size = new System.Drawing.Size(95, 15); label7.TabIndex = 16; - label7.Text = "Exporable Types"; + label7.Text = "Exportable Types"; // // exportableTypes // diff --git a/AssetStudioGUI/Properties/Settings.Designer.cs b/AssetStudioGUI/Properties/Settings.Designer.cs index 48675a2..455994a 100644 --- a/AssetStudioGUI/Properties/Settings.Designer.cs +++ b/AssetStudioGUI/Properties/Settings.Designer.cs @@ -347,18 +347,6 @@ namespace AssetStudioGUI.Properties { } } - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("True")] - public bool exportMiHoYoBinData { - get { - return ((bool)(this["exportMiHoYoBinData"])); - } - set { - this["exportMiHoYoBinData"] = value; - } - } - [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("True")] diff --git a/AssetStudioGUI/Properties/Settings.settings b/AssetStudioGUI/Properties/Settings.settings index cc68ddd..bf4a6ef 100644 --- a/AssetStudioGUI/Properties/Settings.settings +++ b/AssetStudioGUI/Properties/Settings.settings @@ -83,9 +83,6 @@ 0 - - True - True diff --git a/AssetStudioGUI/Studio.cs b/AssetStudioGUI/Studio.cs index fae5e2b..3450ce9 100644 --- a/AssetStudioGUI/Studio.cs +++ b/AssetStudioGUI/Studio.cs @@ -7,8 +7,8 @@ using System.IO; using System.Linq; using System.Threading; using System.Windows.Forms; +using System.Xml; using System.Xml.Linq; -using static AssetStudio.ImportHelper; using static AssetStudioGUI.Exporter; using Object = AssetStudio.Object; @@ -220,7 +220,10 @@ namespace AssetStudioGUI if (int.TryParse(asset.Container, out var value)) { var last = unchecked((uint)value); - var path = ResourceIndex.GetAssetPath(last); + var name = Path.GetFileNameWithoutExtension(asset.SourceFile.originalPath); + if (uint.TryParse(name, out var id)) + { + var path = ResourceIndex.GetContainer(id, last); if (!string.IsNullOrEmpty(path)) { asset.Container = path; @@ -231,6 +234,7 @@ namespace AssetStudioGUI } } } + } Logger.Info("Updated !!"); } } @@ -239,12 +243,12 @@ namespace AssetStudioGUI { StatusStripUpdate("Building asset list..."); + int i = 0; string productName = null; var objectCount = assetsManager.assetsFileList.Sum(x => x.Objects.Count); var objectAssetItemDic = new Dictionary(objectCount); var mihoyoBinDataNames = new List<(PPtr, string)>(); var containers = new List<(PPtr, string)>(); - int i = 0; Progress.Reset(); foreach (var assetsFile in assetsManager.assetsFileList) { @@ -257,7 +261,7 @@ namespace AssetStudioGUI } var assetItem = new AssetItem(asset); objectAssetItemDic.Add(asset, assetItem); - assetItem.UniqueID = " #" + i; + assetItem.UniqueID = "#" + i; var exportable = false; switch (asset) { @@ -340,7 +344,7 @@ namespace AssetStudioGUI assetItem.Text = "IndexObject"; break; case MiHoYoBinData m_MiHoYoBinData: - exportable = MiHoYoBinData.Exportable; + exportable = true; break; case ResourceManager m_ResourceManager: foreach (var m_Container in m_ResourceManager.m_Container) @@ -629,25 +633,30 @@ namespace AssetStudioGUI { 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("Size", asset.FullSize) - ) - ) - ) - ); - - doc.Save(filename); - + var settings = new XmlWriterSettings() { Indent = true }; + using (XmlWriter writer = XmlWriter.Create(filename, settings)) + { + writer.WriteStartDocument(); + writer.WriteStartElement("Assets"); + writer.WriteAttributeString("filename", filename); + writer.WriteAttributeString("createdAt", DateTime.UtcNow.ToString("s")); + foreach (var asset in toExportAssets) + { + writer.WriteStartElement("Asset"); + writer.WriteElementString("Name", asset.Name); + writer.WriteElementString("Container", asset.Container); + writer.WriteStartElement("Type"); + writer.WriteAttributeString("id", ((int)asset.Type).ToString()); + writer.WriteValue(asset.TypeString); + writer.WriteEndElement(); + writer.WriteElementString("PathID", asset.m_PathID.ToString()); + writer.WriteElementString("Source", asset.SourceFile.fullName); + writer.WriteElementString("Size", asset.FullSize.ToString()); + writer.WriteEndElement(); + } + writer.WriteEndElement(); + writer.WriteEndDocument(); + } break; }