using System; using System.Buffers; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; using System.Text; using System.Threading; using static AssetStudio.ImportHelper; namespace AssetStudio { public class AssetsManager { public Game Game; public bool Silent = false; public bool SkipProcess = false; public bool ResolveDependencies = false; public string SpecifyUnityVersion; public CancellationTokenSource tokenSource = new CancellationTokenSource(); public List assetsFileList = new List(); internal Dictionary assetsFileIndexCache = new Dictionary(StringComparer.OrdinalIgnoreCase); internal Dictionary resourceFileReaders = new Dictionary(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 void LoadFiles(string file) { if (Silent) { Logger.Silent = true; Progress.Silent = true; } Load(new string[] { file }); if (Silent) { Logger.Silent = false; Progress.Silent = false; } } public void LoadFiles(params string[] files) { if (Silent) { Logger.Silent = true; Progress.Silent = true; } var path = Path.GetDirectoryName(Path.GetFullPath(files[0])); MergeSplitAssets(path); var toReadFile = ProcessingSplitFiles(files.ToList()); if (ResolveDependencies) toReadFile = AssetsHelper.ProcessDependencies(toReadFile); Load(toReadFile); if (Silent) { Logger.Silent = false; Progress.Silent = false; } } public void LoadFolder(string path) { if (Silent) { Logger.Silent = true; Progress.Silent = true; } MergeSplitAssets(path, true); var files = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories).ToList(); var toReadFile = ProcessingSplitFiles(files); Load(toReadFile); if (Silent) { Logger.Silent = false; Progress.Silent = false; } } private void Load(string[] files) { foreach (var file in files) { Logger.Verbose($"caching {file} path and name to filter out duplicates"); importFiles.Add(file); importFilesHash.Add(Path.GetFileName(file)); } Progress.Reset(); //use a for loop because list size can change for (var i = 0; i < importFiles.Count; i++) { LoadFile(importFiles[i]); Progress.Report(i + 1, importFiles.Count); if (tokenSource.IsCancellationRequested) { Logger.Info("Loading files has been aborted !!"); break; } } importFiles.Clear(); importFilesHash.Clear(); noexistFiles.Clear(); assetsFileListHash.Clear(); AssetsHelper.ClearOffsets(); if (!SkipProcess) { ReadAssets(); ProcessAssets(); } } private void LoadFile(string fullName) { var reader = new FileReader(fullName); reader = reader.PreProcessing(Game); LoadFile(reader); } private void LoadFile(FileReader reader) { switch (reader.FileType) { case FileType.AssetsFile: LoadAssetsFile(reader); break; case FileType.BundleFile: LoadBundleFile(reader); break; case FileType.WebFile: LoadWebFile(reader); break; case FileType.GZipFile: LoadFile(DecompressGZip(reader)); break; case FileType.BrotliFile: LoadFile(DecompressBrotli(reader)); break; case FileType.ZipFile: LoadZipFile(reader); break; case FileType.BlockFile: LoadBlockFile(reader); break; case FileType.BlkFile: LoadBlkFile(reader); break; } } private void LoadAssetsFile(FileReader reader) { if (!assetsFileListHash.Contains(reader.FileName)) { Logger.Info($"Loading {reader.FullPath}"); try { var assetsFile = new SerializedFile(reader, this); CheckStrippedVersion(assetsFile); assetsFileList.Add(assetsFile); assetsFileListHash.Add(assetsFile.fileName); foreach (var sharedFile in assetsFile.m_Externals) { Logger.Verbose($"{assetsFile.fileName} needs external file {sharedFile.fileName}, attempting to look it up..."); var sharedFileName = sharedFile.fileName; if (!importFilesHash.Contains(sharedFileName)) { var sharedFilePath = Path.Combine(Path.GetDirectoryName(reader.FullPath), sharedFileName); if (!noexistFiles.Contains(sharedFilePath)) { if (!File.Exists(sharedFilePath)) { var findFiles = Directory.GetFiles(Path.GetDirectoryName(reader.FullPath), sharedFileName, SearchOption.AllDirectories); if (findFiles.Length > 0) { Logger.Verbose($"Found {findFiles.Length} matching files, picking first file {findFiles[0]} !!"); sharedFilePath = findFiles[0]; } } if (File.Exists(sharedFilePath)) { importFiles.Add(sharedFilePath); importFilesHash.Add(sharedFileName); } else { Logger.Verbose("Nothing was found, caching into non existant files to avoid repeated searching !!"); noexistFiles.Add(sharedFilePath); } } } } } catch (Exception e) { Logger.Error($"Error while reading assets file {reader.FullPath}", e); reader.Dispose(); } } else { Logger.Info($"Skipping {reader.FullPath}"); reader.Dispose(); } } private void LoadAssetsFromMemory(FileReader reader, string originalPath, string unityVersion = null, long originalOffset = 0) { Logger.Verbose($"Loading asset file {reader.FileName} with version {unityVersion} from {originalPath} at offset 0x{originalOffset:X8}"); if (!assetsFileListHash.Contains(reader.FileName)) { try { var assetsFile = new SerializedFile(reader, this); assetsFile.originalPath = originalPath; assetsFile.offset = originalOffset; if (!string.IsNullOrEmpty(unityVersion) && assetsFile.header.m_Version < SerializedFileFormatVersion.Unknown_7) { assetsFile.SetVersion(unityVersion); } CheckStrippedVersion(assetsFile); assetsFileList.Add(assetsFile); assetsFileListHash.Add(assetsFile.fileName); } catch (Exception e) { Logger.Error($"Error while reading assets file {reader.FullPath} from {Path.GetFileName(originalPath)}", e); resourceFileReaders.Add(reader.FileName, reader); } } else Logger.Info($"Skipping {originalPath} ({reader.FileName})"); } private void LoadBundleFile(FileReader reader, string originalPath = null, long originalOffset = 0, bool log = true) { if (log) { Logger.Info("Loading " + reader.FullPath); } try { var bundleFile = new BundleFile(reader, Game); foreach (var file in bundleFile.fileList) { var dummyPath = Path.Combine(Path.GetDirectoryName(reader.FullPath), file.fileName); var subReader = new FileReader(dummyPath, file.stream); if (subReader.FileType == FileType.AssetsFile) { LoadAssetsFromMemory(subReader, originalPath ?? reader.FullPath, bundleFile.m_Header.unityRevision, originalOffset); } else { Logger.Verbose("Caching resource stream"); resourceFileReaders[file.fileName] = subReader; //TODO } } } catch (InvalidCastException) { Logger.Error($"Game type mismatch, Expected {nameof(Mr0k)} but got {Game.Name} ({Game.GetType().Name}) !!"); } catch (Exception e) { var str = $"Error while reading bundle file {reader.FullPath}"; if (originalPath != null) { str += $" from {Path.GetFileName(originalPath)}"; } Logger.Error(str, e); } finally { reader.Dispose(); } } private void LoadWebFile(FileReader reader) { Logger.Info("Loading " + reader.FullPath); try { var webFile = new WebFile(reader); foreach (var file in webFile.fileList) { var dummyPath = Path.Combine(Path.GetDirectoryName(reader.FullPath), file.fileName); var subReader = new FileReader(dummyPath, file.stream); switch (subReader.FileType) { case FileType.AssetsFile: LoadAssetsFromMemory(subReader, reader.FullPath); break; case FileType.BundleFile: LoadBundleFile(subReader, reader.FullPath); break; case FileType.WebFile: LoadWebFile(subReader); break; case FileType.ResourceFile: Logger.Verbose("Caching resource stream"); resourceFileReaders[file.fileName] = subReader; //TODO break; } } } catch (Exception e) { Logger.Error($"Error while reading web file {reader.FullPath}", e); } finally { reader.Dispose(); } } private void LoadZipFile(FileReader reader) { Logger.Info("Loading " + reader.FileName); try { using (ZipArchive archive = new ZipArchive(reader.BaseStream, ZipArchiveMode.Read)) { List splitFiles = new List(); Logger.Verbose("Register all files before parsing the assets so that the external references can be found and find split files"); foreach (ZipArchiveEntry entry in archive.Entries) { if (entry.Name.Contains(".split")) { string baseName = Path.GetFileNameWithoutExtension(entry.Name); string basePath = Path.Combine(Path.GetDirectoryName(entry.FullName), baseName); if (!splitFiles.Contains(basePath)) { splitFiles.Add(basePath); importFilesHash.Add(baseName); } } else { importFilesHash.Add(entry.Name); } } Logger.Verbose("Merge split files and load the result"); foreach (string basePath in splitFiles) { try { Stream splitStream = new MemoryStream(); int i = 0; while (true) { string path = $"{basePath}.split{i++}"; ZipArchiveEntry entry = archive.GetEntry(path); if (entry == null) break; using (Stream entryStream = entry.Open()) { entryStream.CopyTo(splitStream); } } splitStream.Seek(0, SeekOrigin.Begin); FileReader entryReader = new FileReader(basePath, splitStream); entryReader = entryReader.PreProcessing(Game); LoadFile(entryReader); } catch (Exception e) { Logger.Error($"Error while reading zip split file {basePath}", e); } } Logger.Verbose("Load all entries"); Logger.Verbose($"Found {archive.Entries.Count} entries"); foreach (ZipArchiveEntry entry in archive.Entries) { try { string dummyPath = Path.Combine(Path.GetDirectoryName(reader.FullPath), reader.FileName, entry.FullName); Logger.Verbose("Create a new stream to store the deflated stream in and keep the data for later extraction"); Stream streamReader = new MemoryStream(); using (Stream entryStream = entry.Open()) { entryStream.CopyTo(streamReader); } streamReader.Position = 0; FileReader entryReader = new FileReader(dummyPath, streamReader); LoadFile(entryReader); if (entryReader.FileType == FileType.ResourceFile) { entryReader.Position = 0; if (!resourceFileReaders.ContainsKey(entry.Name)) { Logger.Verbose("Caching resource file"); resourceFileReaders.Add(entry.Name, entryReader); } } } catch (Exception e) { Logger.Error($"Error while reading zip entry {entry.FullName}", e); } } } } catch (Exception e) { Logger.Error($"Error while reading zip file {reader.FileName}", e); } finally { reader.Dispose(); } } private void LoadBlockFile(FileReader reader) { Logger.Info("Loading " + reader.FullPath); try { using var stream = new OffsetStream(reader.BaseStream, 0); if (AssetsHelper.TryGet(reader.FullPath, out var offsets)) { foreach (var offset in offsets) { LoadBlockSubFile(reader.FullPath, stream, offset); } } else { do { LoadBlockSubFile(reader.FullPath, stream, stream.AbsolutePosition); } while (stream.Remaining > 0); } } catch (Exception e) { Logger.Error($"Error while reading block file {reader.FileName}", e); } finally { reader.Dispose(); } } private void LoadBlockSubFile(string path, OffsetStream stream, long offset) { var name = offset.ToString("X8"); Logger.Info($"Loading Block {name}"); stream.Offset = offset; var dummyPath = Path.Combine(Path.GetDirectoryName(path), name); var subReader = new FileReader(dummyPath, stream, true); LoadBundleFile(subReader, path, offset, false); } private void LoadBlkFile(FileReader reader) { Logger.Info("Loading " + reader.FullPath); try { using var stream = BlkUtils.Decrypt(reader, (Blk)Game); foreach (var offset in stream.GetOffsets(reader.FullPath)) { var name = offset.ToString("X8"); Logger.Info($"Loading Block {name}"); var dummyPath = Path.Combine(Path.GetDirectoryName(reader.FullPath), name); var subReader = new FileReader(dummyPath, stream, true); switch (subReader.FileType) { case FileType.BundleFile: LoadBundleFile(subReader, reader.FullPath, offset, false); break; case FileType.Mhy0File: LoadMhy0File(subReader, reader.FullPath, offset, false); break; } } } catch (InvalidCastException) { Logger.Error($"Game type mismatch, Expected {nameof(Blk)} but got {Game.Name} ({Game.GetType().Name}) !!"); } catch (Exception e) { Logger.Error($"Error while reading blk file {reader.FileName}", e); } finally { reader.Dispose(); } } private void LoadMhy0File(FileReader reader, string originalPath = null, long originalOffset = 0, bool log = true) { if (log) { Logger.Info("Loading " + reader.FullPath); } try { var mhy0File = new Mhy0File(reader, reader.FullPath, (Mhy0)Game); Logger.Verbose($"mhy0 total size: {mhy0File.TotalSize:X8}"); foreach (var file in mhy0File.fileList) { var dummyPath = Path.Combine(Path.GetDirectoryName(reader.FullPath), file.fileName); var cabReader = new FileReader(dummyPath, file.stream); if (cabReader.FileType == FileType.AssetsFile) { LoadAssetsFromMemory(cabReader, originalPath ?? reader.FullPath, mhy0File.m_Header.unityRevision, originalOffset); } else { Logger.Verbose("Caching resource stream"); resourceFileReaders[file.fileName] = cabReader; //TODO } } } catch (InvalidCastException) { Logger.Error($"Game type mismatch, Expected {nameof(Mhy0)} but got {Game.Name} ({Game.GetType().Name}) !!"); } catch (Exception e) { var str = $"Error while reading mhy0 file {reader.FullPath}"; if (originalPath != null) { str += $" from {Path.GetFileName(originalPath)}"; } Logger.Error(str, e); } finally { reader.Dispose(); } } public void CheckStrippedVersion(SerializedFile assetsFile) { if (assetsFile.IsVersionStripped && string.IsNullOrEmpty(SpecifyUnityVersion)) { throw new Exception("The Unity version has been stripped, please set the version in the options"); } if (!string.IsNullOrEmpty(SpecifyUnityVersion)) { assetsFile.SetVersion(SpecifyUnityVersion); } } public void Clear() { Logger.Verbose("Cleaning up..."); foreach (var assetsFile in assetsFileList) { assetsFile.Objects.Clear(); assetsFile.reader.Close(); } assetsFileList.Clear(); foreach (var resourceFileReader in resourceFileReaders) { resourceFileReader.Value.Close(); } resourceFileReaders.Clear(); assetsFileIndexCache.Clear(); tokenSource.Dispose(); tokenSource = new CancellationTokenSource(); GC.WaitForPendingFinalizers(); GC.Collect(); } private void ReadAssets() { Logger.Info("Read assets..."); var progressCount = assetsFileList.Sum(x => x.m_Objects.Count); int i = 0; Progress.Reset(); foreach (var assetsFile in assetsFileList) { foreach (var objectInfo in assetsFile.m_Objects) { if (tokenSource.IsCancellationRequested) { Logger.Info("Reading assets has been cancelled !!"); return; } var objectReader = new ObjectReader(assetsFile.reader, assetsFile, objectInfo, Game); try { Object obj = objectReader.type switch { ClassIDType.Animation => new Animation(objectReader), ClassIDType.AnimationClip when AnimationClip.Parsable => new AnimationClip(objectReader), ClassIDType.Animator => new Animator(objectReader), ClassIDType.AnimatorController => new AnimatorController(objectReader), ClassIDType.AnimatorOverrideController => new AnimatorOverrideController(objectReader), ClassIDType.AssetBundle => new AssetBundle(objectReader), ClassIDType.AudioClip => new AudioClip(objectReader), ClassIDType.Avatar => new Avatar(objectReader), ClassIDType.Font => new Font(objectReader), ClassIDType.GameObject => new GameObject(objectReader), ClassIDType.IndexObject => new IndexObject(objectReader), ClassIDType.Material => new Material(objectReader), ClassIDType.Mesh => new Mesh(objectReader), ClassIDType.MeshFilter => new MeshFilter(objectReader), ClassIDType.MeshRenderer when Renderer.Parsable => new MeshRenderer(objectReader), ClassIDType.MiHoYoBinData => new MiHoYoBinData(objectReader), ClassIDType.MonoBehaviour => new MonoBehaviour(objectReader), ClassIDType.MonoScript => new MonoScript(objectReader), ClassIDType.MovieTexture => new MovieTexture(objectReader), ClassIDType.PlayerSettings => new PlayerSettings(objectReader), ClassIDType.RectTransform => new RectTransform(objectReader), ClassIDType.Shader when Shader.Parsable => new Shader(objectReader), ClassIDType.SkinnedMeshRenderer when Renderer.Parsable => new SkinnedMeshRenderer(objectReader), ClassIDType.Sprite => new Sprite(objectReader), ClassIDType.SpriteAtlas => new SpriteAtlas(objectReader), ClassIDType.TextAsset => new TextAsset(objectReader), ClassIDType.Texture2D => new Texture2D(objectReader), ClassIDType.Transform => new Transform(objectReader), ClassIDType.VideoClip => new VideoClip(objectReader), ClassIDType.ResourceManager => new ResourceManager(objectReader), _ => new Object(objectReader), }; assetsFile.AddObject(obj); } catch (Exception e) { var sb = new StringBuilder(); sb.AppendLine("Unable to load object") .AppendLine($"Assets {assetsFile.fileName}") .AppendLine($"Path {assetsFile.originalPath}") .AppendLine($"Type {objectReader.type}") .AppendLine($"PathID {objectInfo.m_PathID}") .Append(e); Logger.Error(sb.ToString()); } Progress.Report(++i, progressCount); } } } private void ProcessAssets() { Logger.Info("Process Assets..."); foreach (var assetsFile in assetsFileList) { foreach (var obj in assetsFile.Objects) { if (tokenSource.IsCancellationRequested) { Logger.Info("Processing assets has been cancelled !!"); return; } if (obj is GameObject m_GameObject) { Logger.Verbose($"GameObject with {m_GameObject.m_PathID} in file {m_GameObject.assetsFile.fileName} has {m_GameObject.m_Components.Length} components, Attempting to fetch them..."); foreach (var pptr in m_GameObject.m_Components) { if (pptr.TryGet(out var m_Component)) { switch (m_Component) { case Transform m_Transform: Logger.Verbose($"Fetched Transform component with {m_Transform.m_PathID} in file {m_Transform.assetsFile.fileName}, assigning to GameObject components..."); m_GameObject.m_Transform = m_Transform; break; case MeshRenderer m_MeshRenderer: Logger.Verbose($"Fetched MeshRenderer component with {m_MeshRenderer.m_PathID} in file {m_MeshRenderer.assetsFile.fileName}, assigning to GameObject components..."); m_GameObject.m_MeshRenderer = m_MeshRenderer; break; case MeshFilter m_MeshFilter: Logger.Verbose($"Fetched MeshFilter component with {m_MeshFilter.m_PathID} in file {m_MeshFilter.assetsFile.fileName}, assigning to GameObject components..."); m_GameObject.m_MeshFilter = m_MeshFilter; break; case SkinnedMeshRenderer m_SkinnedMeshRenderer: Logger.Verbose($"Fetched SkinnedMeshRenderer component with {m_SkinnedMeshRenderer.m_PathID} in file {m_SkinnedMeshRenderer.assetsFile.fileName}, assigning to GameObject components..."); m_GameObject.m_SkinnedMeshRenderer = m_SkinnedMeshRenderer; break; case Animator m_Animator: Logger.Verbose($"Fetched Animator component with {m_Animator.m_PathID} in file {m_Animator.assetsFile.fileName}, assigning to GameObject components..."); m_GameObject.m_Animator = m_Animator; break; case Animation m_Animation: Logger.Verbose($"Fetched Animation component with {m_Animation.m_PathID} in file {m_Animation.assetsFile.fileName}, assigning to GameObject components..."); m_GameObject.m_Animation = m_Animation; break; } } } } else if (obj is SpriteAtlas m_SpriteAtlas) { if (m_SpriteAtlas.m_RenderDataMap.Count > 0) { Logger.Verbose($"SpriteAtlas with {m_SpriteAtlas.m_PathID} in file {m_SpriteAtlas.assetsFile.fileName} has {m_SpriteAtlas.m_PackedSprites.Length} packed sprites, Attempting to fetch them..."); foreach (var m_PackedSprite in m_SpriteAtlas.m_PackedSprites) { if (m_PackedSprite.TryGet(out var m_Sprite)) { if (m_Sprite.m_SpriteAtlas.IsNull) { Logger.Verbose($"Fetched Sprite with {m_Sprite.m_PathID} in file {m_Sprite.assetsFile.fileName}, assigning to parent SpriteAtlas..."); m_Sprite.m_SpriteAtlas.Set(m_SpriteAtlas); } else { m_Sprite.m_SpriteAtlas.TryGet(out var m_SpriteAtlaOld); if (m_SpriteAtlaOld.m_IsVariant) { Logger.Verbose($"Fetched Sprite with {m_Sprite.m_PathID} in file {m_Sprite.assetsFile.fileName} has a variant of the origianl SpriteAtlas, disposing of the variant and assinging to the parent SpriteAtlas..."); m_Sprite.m_SpriteAtlas.Set(m_SpriteAtlas); } } } } } } } } } } }