diff --git a/AssetStudio.CLI/Components/CommandLine.cs b/AssetStudio.CLI/Components/CommandLine.cs index 13b70f08..63291c01 100644 --- a/AssetStudio.CLI/Components/CommandLine.cs +++ b/AssetStudio.CLI/Components/CommandLine.cs @@ -5,6 +5,7 @@ using System.CommandLine.Binding; using System.CommandLine.Parsing; using System.Text.RegularExpressions; +using System.Collections.Generic; namespace AssetStudio.CLI { @@ -49,7 +50,7 @@ public class Options { public bool Silent { get; set; } public LoggerEvent[] LoggerFlags { get; set; } - public ClassIDType[] TypeFilter { get; set; } + public string[] TypeFilter { get; set; } public Regex[] NameFilter { get; set; } public Regex[] ContainerFilter { get; set; } public string GameName { get; set; } @@ -71,7 +72,7 @@ public class OptionsBinder : BinderBase { public readonly Option Silent; public readonly Option LoggerFlags; - public readonly Option TypeFilter; + public readonly Option TypeFilter; public readonly Option NameFilter; public readonly Option ContainerFilter; public readonly Option GameName; @@ -92,9 +93,69 @@ public OptionsBinder() { Silent = new Option("--silent", "Hide log messages."); LoggerFlags = new Option("--logger_flags", "Flags to control toggle log events.") { AllowMultipleArgumentsPerToken = true, ArgumentHelpName = "Verbose|Debug|Info|etc.." }; - 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 }; + TypeFilter = new Option("--types", "Specify unity class type(s)") { AllowMultipleArgumentsPerToken = true, ArgumentHelpName = "Texture2D|Shader:Parse|Sprite:Both|etc.." }; + NameFilter = new Option("--names", result => + { + var items = new List(); + var value = result.Tokens.Single().Value; + if (File.Exists(value)) + { + var lines = File.ReadLines(value); + foreach (var line in lines) + { + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + try + { + items.Add(new Regex(line, RegexOptions.IgnoreCase)); + } + catch (ArgumentException e) + { + continue; + } + } + } + else + { + items.AddRange(result.Tokens.Select(x => new Regex(x.Value, RegexOptions.IgnoreCase)).ToArray()); + } + + return items.ToArray(); + }, false, "Specify name regex filter(s).") { AllowMultipleArgumentsPerToken = true }; + ContainerFilter = new Option("--containers", result => + { + var items = new List(); + var value = result.Tokens.Single().Value; + if (File.Exists(value)) + { + var lines = File.ReadLines(value); + foreach(var line in lines) + { + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + try + { + items.Add(new Regex(line, RegexOptions.IgnoreCase)); + } + catch (ArgumentException e) + { + continue; + } + } + } + else + { + items.AddRange(result.Tokens.Select(x => new Regex(x.Value, RegexOptions.IgnoreCase)).ToArray()); + } + + return items.ToArray(); + }, false, "Specify container regex filter(s).") { AllowMultipleArgumentsPerToken = true }; GameName = new Option("--game", $"Specify Game.") { IsRequired = true }; KeyIndex = new Option("--key_index", "Specify key index.") { ArgumentHelpName = UnityCNManager.ToString() }; MapOp = new Option("--map_op", "Specify which map to build."); @@ -110,16 +171,7 @@ public OptionsBinder() 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); - } + return ParseKey(result.Tokens.Single().Value); }, false, "XOR key to decrypt MiHoYoBinData."); LoggerFlags.AddValidator(FilterValidator); @@ -131,15 +183,7 @@ public OptionsBinder() var value = result.Tokens.Single().Value; try { - if (value.StartsWith("0x")) - { - value = value.Substring(2); - Convert.ToByte(value, 0x10); - } - else - { - byte.Parse(value); - } + ParseKey(value); } catch (Exception e) { @@ -156,6 +200,19 @@ public OptionsBinder() MapType.SetDefaultValue(ExportListType.XML); KeyIndex.SetDefaultValue(0); } + + public byte ParseKey(string value) + { + if (value.StartsWith("0x")) + { + value = value[2..]; + return Convert.ToByte(value, 0x10); + } + else + { + return byte.Parse(value); + } + } public void FilterValidator(OptionResult result) { diff --git a/AssetStudio.CLI/Exporter.cs b/AssetStudio.CLI/Exporter.cs index 3664154f..b25cc5a7 100644 --- a/AssetStudio.CLI/Exporter.cs +++ b/AssetStudio.CLI/Exporter.cs @@ -393,7 +393,7 @@ public static bool ExportGameObject(AssetItem item, string exportPath, List animationList = null) diff --git a/AssetStudio.CLI/Program.cs b/AssetStudio.CLI/Program.cs index e7b0da90..df70beef 100644 --- a/AssetStudio.CLI/Program.cs +++ b/AssetStudio.CLI/Program.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading; +using System.Threading.Tasks; using AssetStudio.CLI.Properties; using Newtonsoft.Json; using static AssetStudio.CLI.Studio; @@ -46,20 +46,62 @@ public static void Run(Options o) AssetsHelper.Minimal = Settings.Default.minimalAssetMap; AssetsHelper.SetUnityVersion(o.UnityVersion); - if (o.TypeFilter == null) - { - TypeFlags.SetTypes(JsonConvert.DeserializeObject>(Settings.Default.types)); - } - else + TypeFlags.SetTypes(JsonConvert.DeserializeObject>(Settings.Default.types)); + + var classTypeFilter = Array.Empty(); + if (!o.TypeFilter.IsNullOrEmpty()) { - foreach (var type in o.TypeFilter) + var exportTexture2D = false; + var exportMaterial = false; + var classTypeFilterList = new List(); + for (int i = 0; i < o.TypeFilter.Length; i++) { - TypeFlags.SetType(type, true, true); + var typeStr = o.TypeFilter[i]; + var type = ClassIDType.UnknownType; + var flag = TypeFlag.Both; + + try + { + if (typeStr.Contains(':')) + { + var param = typeStr.Split(':'); + + flag = (TypeFlag)Enum.Parse(typeof(TypeFlag), param[1], true); + + typeStr = param[0]; + } + + type = (ClassIDType)Enum.Parse(typeof(ClassIDType), typeStr, true); + + if (type == ClassIDType.Texture2D) + { + exportTexture2D = flag.HasFlag(TypeFlag.Export); + } + else if (type == ClassIDType.Material) + { + exportMaterial = flag.HasFlag(TypeFlag.Export); + } + + TypeFlags.SetType(type, flag.HasFlag(TypeFlag.Parse), flag.HasFlag(TypeFlag.Export)); + + classTypeFilterList.Add(type); + } + catch(Exception e) + { + Logger.Error($"{typeStr} has invalid format, skipping..."); + continue; + } } + classTypeFilter = classTypeFilterList.ToArray(); + if (ClassIDType.GameObject.CanExport() || ClassIDType.Animator.CanExport()) { - TypeFlags.SetType(ClassIDType.Texture2D, true, false); + TypeFlags.SetType(ClassIDType.Texture2D, true, exportTexture2D); + if (Settings.Default.exportMaterials) + { + TypeFlags.SetType(ClassIDType.Material, true, exportMaterial); + } if (ClassIDType.GameObject.CanExport()) { TypeFlags.SetType(ClassIDType.Animator, true, false); @@ -68,13 +110,14 @@ public static void Run(Options o) { TypeFlags.SetType(ClassIDType.GameObject, true, false); } - if (Settings.Default.exportMaterials) - { - TypeFlags.SetType(ClassIDType.Material, true, false); - } } } + if (o.GroupAssetsType == AssetGroupOption.ByContainer) + { + TypeFlags.SetType(ClassIDType.AssetBundle, true, false); + } + assetsManager.Silent = o.Silent; assetsManager.Game = game; assetsManager.SpecifyUnityVersion = o.UnityVersion; @@ -102,24 +145,30 @@ public static void Run(Options o) if (o.MapOp.HasFlag(MapOpType.CABMap)) { - AssetsHelper.BuildCABMap(files, o.MapName, o.Input.FullName, game); - } - if (o.MapOp.HasFlag(MapOpType.Load)) - { - AssetsHelper.LoadCABMapInternal(o.MapName); - assetsManager.ResolveDependencies = true; + if (o.MapOp.HasFlag(MapOpType.Load)) + { + AssetsHelper.BuildCABMap(files, o.MapName, o.Input.FullName, game); + } + else + { + AssetsHelper.LoadCABMapInternal(o.MapName); + assetsManager.ResolveDependencies = true; + } } if (o.MapOp.HasFlag(MapOpType.AssetMap)) { - if (files.Length == 1) + if (o.MapOp.HasFlag(MapOpType.Load)) + { + files = AssetsHelper.ParseAssetMap(o.MapName, o.MapType, classTypeFilter, o.NameFilter, o.ContainerFilter); + } + else { - throw new Exception("Unable to build AssetMap with input_path as a file !!"); + Task.Run(() => AssetsHelper.BuildAssetMap(files, o.MapName, game, o.Output.FullName, o.MapType, classTypeFilter, o.NameFilter, o.ContainerFilter)).Wait(); } - AssetsHelper.BuildAssetMap(files, o.MapName, game, o.Output.FullName, o.MapType, o.TypeFilter, o.NameFilter, o.ContainerFilter); } if (o.MapOp.HasFlag(MapOpType.Both)) { - AssetsHelper.BuildBoth(files, o.MapName, o.Input.FullName, game, o.Output.FullName, o.MapType, o.TypeFilter, o.NameFilter, o.ContainerFilter); + Task.Run(() => AssetsHelper.BuildBoth(files, o.MapName, o.Input.FullName, game, o.Output.FullName, o.MapType, classTypeFilter, o.NameFilter, o.ContainerFilter)).Wait(); } if (o.MapOp.Equals(MapOpType.None) || o.MapOp.HasFlag(MapOpType.Load)) { @@ -135,7 +184,7 @@ public static void Run(Options o) assetsManager.LoadFiles(file); if (assetsManager.assetsFileList.Count > 0) { - BuildAssetData(o.TypeFilter, o.NameFilter, o.ContainerFilter, ref i); + BuildAssetData(classTypeFilter, o.NameFilter, o.ContainerFilter, ref i); ExportAssets(o.Output.FullName, exportableAssets, o.GroupAssetsType, o.AssetExportType); } exportableAssets.Clear(); diff --git a/AssetStudio.CLI/Studio.cs b/AssetStudio.CLI/Studio.cs index 9d5d4854..ceee7b55 100644 --- a/AssetStudio.CLI/Studio.cs +++ b/AssetStudio.CLI/Studio.cs @@ -235,7 +235,7 @@ public static void BuildAssetData(ClassIDType[] typeFilters, Regex[] nameFilters { foreach (var asset in assetsFile.Objects) { - ProcessAssetData(asset, typeFilters, nameFilters, objectAssetItemDic, mihoyoBinDataNames, containers, ref i); + ProcessAssetData(asset, objectAssetItemDic, mihoyoBinDataNames, containers, ref i); } } foreach ((var pptr, var name) in mihoyoBinDataNames) @@ -257,15 +257,7 @@ public static void BuildAssetData(ClassIDType[] typeFilters, Regex[] nameFilters { if (pptr.TryGet(out var obj)) { - var item = objectAssetItemDic[obj]; - if (containerFilters.IsNullOrEmpty() || containerFilters.Any(x => x.IsMatch(container))) - { - item.Container = container; - } - else - { - exportableAssets.Remove(item); - } + objectAssetItemDic[obj].Container = container; } } containers.Clear(); @@ -274,9 +266,19 @@ public static void BuildAssetData(ClassIDType[] typeFilters, Regex[] nameFilters UpdateContainers(); } } + + var matches = exportableAssets.Where(x => + { + var isMatchRegex = nameFilters.IsNullOrEmpty() || nameFilters.Any(y => y.IsMatch(x.Text)); + var isFilteredType = typeFilters.IsNullOrEmpty() || typeFilters.Contains(x.Type); + var isContainerMatch = containerFilters.IsNullOrEmpty() || containerFilters.Any(y => y.IsMatch(x.Container)); + return isMatchRegex && isFilteredType && isContainerMatch; + }).ToArray(); + exportableAssets.Clear(); + exportableAssets.AddRange(matches); } - public static void ProcessAssetData(Object asset, ClassIDType[] typeFilters, Regex[] nameFilters, Dictionary objectAssetItemDic, List<(PPtr, string)> mihoyoBinDataNames, List<(PPtr, string)> containers, ref int i) + public static void ProcessAssetData(Object asset, Dictionary objectAssetItemDic, List<(PPtr, string)> mihoyoBinDataNames, List<(PPtr, string)> containers, ref int i) { var assetItem = new AssetItem(asset); objectAssetItemDic.Add(asset, assetItem); @@ -352,10 +354,8 @@ public static void ProcessAssetData(Object asset, ClassIDType[] typeFilters, Reg { assetItem.Text = assetItem.TypeString + assetItem.UniqueID; } - - var isMatchRegex = nameFilters.IsNullOrEmpty() || nameFilters.Any(x => x.IsMatch(assetItem.Text)); - var isFilteredType = typeFilters.IsNullOrEmpty() || typeFilters.Contains(assetItem.Type); - if (isMatchRegex && isFilteredType && exportable) + + if (exportable) { exportableAssets.Add(assetItem); } diff --git a/AssetStudio.GUI/AssetBrowser.cs b/AssetStudio.GUI/AssetBrowser.cs index c04c8749..ca5a8a00 100644 --- a/AssetStudio.GUI/AssetBrowser.cs +++ b/AssetStudio.GUI/AssetBrowser.cs @@ -126,7 +126,7 @@ private void BuildAssetData(List exportableAssets, AssetEntry[] entri { foreach (var asset in assetsFile.Objects) { - ProcessAssetData(asset, exportableAssets, entries, objectAssetItemDic, mihoyoBinDataNames, containers); + ProcessAssetData(asset, exportableAssets, objectAssetItemDic, mihoyoBinDataNames, containers); } } foreach ((var pptr, var name) in mihoyoBinDataNames) @@ -151,16 +151,12 @@ private void BuildAssetData(List exportableAssets, AssetEntry[] entri } } containers.Clear(); - for (int i = exportableAssets.Count - 1; i >= 0; i--) - { - var asset = exportableAssets[i]; - if (!entries.Any(x => x.Container == asset.Container && x.Name == asset.Text && x.Type == asset.Type && x.PathID == asset.m_PathID)) - { - exportableAssets.Remove(asset); - } - } + + var matches = exportableAssets.Where(asset => entries.Any(x => x.Container == asset.Container && x.Name == asset.Text && x.Type == asset.Type && x.PathID == asset.m_PathID)).ToArray(); + exportableAssets.Clear(); + exportableAssets.AddRange(matches); } - private void ProcessAssetData(Object asset, List exportableAssets, AssetEntry[] entries, Dictionary objectAssetItemDic, List<(PPtr, string)> mihoyoBinDataNames, List<(PPtr, string)> containers) + private void ProcessAssetData(Object asset, List exportableAssets, Dictionary objectAssetItemDic, List<(PPtr, string)> mihoyoBinDataNames, List<(PPtr, string)> containers) { var assetItem = new AssetItem(asset); objectAssetItemDic.Add(asset, assetItem); @@ -220,8 +216,8 @@ private void ProcessAssetData(Object asset, List exportableAssets, As break; case Mesh _ when ClassIDType.Mesh.CanExport(): case TextAsset _ when ClassIDType.TextAsset.CanExport(): - case AnimationClip _ when ClassIDType.Font.CanExport(): - case Font _ when ClassIDType.GameObject.CanExport(): + case AnimationClip _ when ClassIDType.AnimationClip.CanExport(): + case Font _ when ClassIDType.Font.CanExport(): case MovieTexture _ when ClassIDType.MovieTexture.CanExport(): case Sprite _ when ClassIDType.Sprite.CanExport(): case Material _ when ClassIDType.Material.CanExport(): @@ -231,12 +227,13 @@ private void ProcessAssetData(Object asset, List exportableAssets, As exportable = true; break; } + if (assetItem.Text == "") { assetItem.Text = assetItem.TypeString + assetItem.UniqueID; } - if (entries.Any(x => x.Name == assetItem.Text && x.Type == assetItem.Type) && exportable) + if (exportable) { exportableAssets.Add(assetItem); } diff --git a/AssetStudio.GUI/AssetStudio.GUI.csproj b/AssetStudio.GUI/AssetStudio.GUI.csproj index 0290b450..8862e011 100644 --- a/AssetStudio.GUI/AssetStudio.GUI.csproj +++ b/AssetStudio.GUI/AssetStudio.GUI.csproj @@ -89,6 +89,25 @@ + + + PreserveNewest + x86\HLSLDecompiler.dll + + + PreserveNewest + x64\HLSLDecompiler.dll + + + PreserveNewest + x86\BinaryDecompiler.lib + + + PreserveNewest + x64\BinaryDecompiler.lib + + + diff --git a/AssetStudio.GUI/Components/GameObjectTreeNode.cs b/AssetStudio.GUI/Components/GameObjectTreeNode.cs index 99c71e6d..6d7eba86 100644 --- a/AssetStudio.GUI/Components/GameObjectTreeNode.cs +++ b/AssetStudio.GUI/Components/GameObjectTreeNode.cs @@ -10,6 +10,10 @@ public GameObjectTreeNode(GameObject gameObject) { this.gameObject = gameObject; Text = gameObject.m_Name; + if (gameObject.HasModel()) + { + BackColor = System.Drawing.Color.LightBlue; + } } } } diff --git a/AssetStudio.GUI/Exporter.cs b/AssetStudio.GUI/Exporter.cs index 1d14c322..a4106fec 100644 --- a/AssetStudio.GUI/Exporter.cs +++ b/AssetStudio.GUI/Exporter.cs @@ -392,7 +392,7 @@ public static bool ExportGameObject(AssetItem item, string exportPath, List animationList = null) diff --git a/AssetStudio.GUI/Libraries/x64/BinaryDecompiler.lib b/AssetStudio.GUI/Libraries/x64/BinaryDecompiler.lib new file mode 100644 index 00000000..7f326224 Binary files /dev/null and b/AssetStudio.GUI/Libraries/x64/BinaryDecompiler.lib differ diff --git a/AssetStudio.GUI/Libraries/x64/HLSLDecompiler.dll b/AssetStudio.GUI/Libraries/x64/HLSLDecompiler.dll new file mode 100644 index 00000000..e55736c1 Binary files /dev/null and b/AssetStudio.GUI/Libraries/x64/HLSLDecompiler.dll differ diff --git a/AssetStudio.GUI/Libraries/x86/BinaryDecompiler.lib b/AssetStudio.GUI/Libraries/x86/BinaryDecompiler.lib new file mode 100644 index 00000000..d50f0af1 Binary files /dev/null and b/AssetStudio.GUI/Libraries/x86/BinaryDecompiler.lib differ diff --git a/AssetStudio.GUI/Libraries/x86/HLSLDecompiler.dll b/AssetStudio.GUI/Libraries/x86/HLSLDecompiler.dll new file mode 100644 index 00000000..d0fda315 Binary files /dev/null and b/AssetStudio.GUI/Libraries/x86/HLSLDecompiler.dll differ diff --git a/AssetStudio.GUI/MainForm.Designer.cs b/AssetStudio.GUI/MainForm.Designer.cs index 954e9a4e..e2a3f6bc 100644 --- a/AssetStudio.GUI/MainForm.Designer.cs +++ b/AssetStudio.GUI/MainForm.Designer.cs @@ -904,7 +904,7 @@ private void InitializeComponent() treeSearch.ForeColor = System.Drawing.SystemColors.WindowText; treeSearch.Location = new System.Drawing.Point(0, 0); treeSearch.Name = "treeSearch"; - treeSearch.PlaceholderText = "Search (with Ctrl to check result, with Shift for all)"; + treeSearch.PlaceholderText = "Search (with Ctrl to check result, with Shift for all, alt for parent nodes)"; treeSearch.Size = new System.Drawing.Size(472, 23); treeSearch.TabIndex = 0; treeSearch.TextChanged += treeSearch_TextChanged; diff --git a/AssetStudio.GUI/MainForm.cs b/AssetStudio.GUI/MainForm.cs index df775c20..1453ef78 100644 --- a/AssetStudio.GUI/MainForm.cs +++ b/AssetStudio.GUI/MainForm.cs @@ -185,7 +185,7 @@ private void MainForm_DragEnter(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(DataFormats.FileDrop)) { - e.Effect = DragDropEffects.Move; + e.Effect = DragDropEffects.Copy; } } @@ -609,9 +609,12 @@ private void treeSearch_KeyDown(object sender, KeyEventArgs e) foreach (var node in treeSrcResults) { var tempNode = node; - while (tempNode.Parent != null) + if (e.Alt) { - tempNode = tempNode.Parent; + while (tempNode.Parent != null) + { + tempNode = tempNode.Parent; + } } tempNode.EnsureVisible(); tempNode.Checked = e.Control; @@ -625,10 +628,14 @@ private void treeSearch_KeyDown(object sender, KeyEventArgs e) nextGObject = 0; } var node = treeSrcResults[nextGObject]; - while (node.Parent != null) + if (e.Alt) { - node = node.Parent; + while (node.Parent != null) + { + node = node.Parent; + } } + node.EnsureVisible(); node.Checked = e.Control; sceneTreeView.SelectedNode = treeSrcResults[nextGObject]; diff --git a/AssetStudio.GUI/Studio.cs b/AssetStudio.GUI/Studio.cs index c503d868..ba1b130d 100644 --- a/AssetStudio.GUI/Studio.cs +++ b/AssetStudio.GUI/Studio.cs @@ -548,7 +548,7 @@ public static Task ExportAssets(string savePath, List toExportAssets, case AssetGroupOption.ByContainer: //container path if (!string.IsNullOrEmpty(asset.Container)) { - exportPath = Path.Combine(savePath, Path.GetDirectoryName(asset.Container)); + exportPath = Path.HasExtension(asset.Container) ? Path.Combine(savePath, Path.GetDirectoryName(asset.Container)) : Path.Combine(savePath, asset.Container); } else { diff --git a/AssetStudio.Utility/AssetStudio.Utility.csproj b/AssetStudio.Utility/AssetStudio.Utility.csproj index 675023c1..e79e7828 100644 --- a/AssetStudio.Utility/AssetStudio.Utility.csproj +++ b/AssetStudio.Utility/AssetStudio.Utility.csproj @@ -15,6 +15,7 @@ + diff --git a/AssetStudio.Utility/ShaderConverter.cs b/AssetStudio.Utility/ShaderConverter.cs index 55b10e16..5a55bc69 100644 --- a/AssetStudio.Utility/ShaderConverter.cs +++ b/AssetStudio.Utility/ShaderConverter.cs @@ -1,11 +1,15 @@ -using SpirV; +using AssetStudio.PInvoke; +using SharpGen.Runtime; +using SpirV; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; +using Vortice.D3DCompiler; namespace AssetStudio { @@ -16,7 +20,7 @@ public static string Convert(this Shader shader) if (shader.m_SubProgramBlob != null) //5.3 - 5.4 { var decompressedBytes = new byte[shader.decompressedSize]; - var numWrite = LZ4.Decompress(shader.m_SubProgramBlob, decompressedBytes); + var numWrite = LZ4.Instance.Decompress(shader.m_SubProgramBlob, decompressedBytes); if (numWrite != shader.decompressedSize) { throw new IOException($"Lz4 decompression error, write {numWrite} bytes but expected {shader.decompressedSize} bytes"); @@ -55,7 +59,7 @@ private static string ConvertSerializedShader(Shader shader) } else { - var numWrite = LZ4.Decompress(shader.compressedBlob.AsSpan().Slice((int)offset, (int)compressedLength), decompressedBytes.AsSpan().Slice(0, (int)decompressedLength)); + var numWrite = LZ4.Instance.Decompress(shader.compressedBlob.AsSpan().Slice((int)offset, (int)compressedLength), decompressedBytes.AsSpan().Slice(0, (int)decompressedLength)); if (numWrite != decompressedLength) { throw new IOException($"Lz4 decompression error, write {numWrite} bytes but expected {decompressedLength} bytes"); @@ -686,7 +690,10 @@ private static string ConvertSerializedProperty(SerializedProperty m_Prop) { sb.Append($"[{m_Attribute}] "); } - //TODO Flag + foreach (var flag in Enum.GetValues().Where(x => m_Prop.m_Flags.HasFlag(x))) + { + sb.Append($"[{flag}] "); + } sb.Append($"{m_Prop.m_Name} (\"{m_Prop.m_Description}\", "); switch (m_Prop.m_Type) { @@ -1031,6 +1038,7 @@ public string Export() case ShaderGpuProgramType.GLCore32: case ShaderGpuProgramType.GLCore41: case ShaderGpuProgramType.GLCore43: + sb.Append($"// hash: {ComputeHash64(m_ProgramCode):x8}\n"); sb.Append(Encoding.UTF8.GetString(m_ProgramCode)); break; case ShaderGpuProgramType.DX9VertexSM20: @@ -1038,9 +1046,19 @@ public string Export() case ShaderGpuProgramType.DX9PixelSM20: case ShaderGpuProgramType.DX9PixelSM30: { - /*var shaderBytecode = new ShaderBytecode(m_ProgramCode); - sb.Append(shaderBytecode.Disassemble());*/ - sb.Append("// shader disassembly not supported on DXBC"); + try + { + var programCodeSpan = m_ProgramCode.AsSpan(); + var g = Compiler.Disassemble(programCodeSpan.GetPinnableReference(), programCodeSpan.Length, DisasmFlags.None, ""); + + sb.Append($"// hash: {ComputeHash64(programCodeSpan):x8}\n"); + sb.Append(g.AsString()); + } + catch (Exception e) + { + sb.Append($"// disassembly error {e.Message}\n"); + } + break; } case ShaderGpuProgramType.DX10Level9Vertex: @@ -1054,20 +1072,48 @@ public string Export() case ShaderGpuProgramType.DX11HullSM50: case ShaderGpuProgramType.DX11DomainSM50: { - /*int start = 6; - if (m_Version == 201509030) // 5.3 + int type = m_ProgramCode[0]; + int start = 1; + if (type > 0) + { + if (type == 1) + { + start = 6; + } + else if (type == 2) + { + start = 38; + } + } + + var buffSpan = m_ProgramCode.AsSpan(start); + + sb.Append($"// hash: {ComputeHash64(buffSpan):x8}\n"); + try + { + HLSLDecompiler.DecompileShader(buffSpan.ToArray(), buffSpan.Length, out var hlslText); + sb.Append(hlslText); + } + catch (Exception e) { - start = 5; + Logger.Verbose($"Decompile error {e.Message}"); + Logger.Verbose($"Attempting to disassemble..."); + + try + { + var g = Compiler.Disassemble(buffSpan.GetPinnableReference(), buffSpan.Length, DisasmFlags.None, ""); + sb.Append(g.AsString()); + } + catch (Exception ex) + { + sb.Append($"// decompile/disassembly error {ex.Message}\n"); + } } - var buff = new byte[m_ProgramCode.Length - start]; - Buffer.BlockCopy(m_ProgramCode, start, buff, 0, buff.Length); - var shaderBytecode = new ShaderBytecode(buff); - sb.Append(shaderBytecode.Disassemble());*/ - sb.Append("// shader disassembly not supported on DXBC"); break; } case ShaderGpuProgramType.MetalVS: case ShaderGpuProgramType.MetalFS: + sb.Append($"// hash: {ComputeHash64(m_ProgramCode):x8}\n"); using (var reader = new EndianBinaryReader(new MemoryStream(m_ProgramCode), EndianType.LittleEndian)) { var fourCC = reader.ReadUInt32(); @@ -1084,6 +1130,7 @@ public string Export() case ShaderGpuProgramType.SPIRV: try { + sb.Append($"// hash: {ComputeHash64(m_ProgramCode):x8}\n"); sb.Append(SpirVShaderConverter.Convert(m_ProgramCode)); } catch (Exception e) @@ -1096,9 +1143,11 @@ public string Export() case ShaderGpuProgramType.ConsoleHS: case ShaderGpuProgramType.ConsoleDS: case ShaderGpuProgramType.ConsoleGS: + sb.Append($"//hash: {ComputeHash64(m_ProgramCode):x8}\n"); sb.Append(Encoding.UTF8.GetString(m_ProgramCode)); break; default: + sb.Append($"//hash: {ComputeHash64(m_ProgramCode):x8}\n"); sb.Append($"//shader disassembly not supported on {m_ProgramType}"); break; } @@ -1106,5 +1155,42 @@ public string Export() sb.Append('"'); return sb.ToString(); } + public ulong ComputeHash64(Span data) + { + ulong hval = 0; + foreach (var b in data) + { + hval *= 0x100000001B3; + hval ^= b; + } + return hval; + } + } + + public static class HLSLDecompiler + { + private const string DLL_NAME = "HLSLDecompiler"; + static HLSLDecompiler() + { + DllLoader.PreloadDll(DLL_NAME); + } + public static void DecompileShader(byte[] shaderByteCode, int shaderByteCodeSize, out string hlslText) + { + var code = Decompile(shaderByteCode, shaderByteCodeSize, out var shaderText, out var shaderTextSize); + if (code != 0) + { + throw new Exception($"Unable to decompile shader, Error code: {code}"); + } + + hlslText = Marshal.PtrToStringAnsi(shaderText, shaderTextSize); + Marshal.FreeHGlobal(shaderText); + } + + #region importfunctions + + [DllImport(DLL_NAME, CallingConvention = CallingConvention.Cdecl)] + private static extern int Decompile(byte[] shaderByteCode, int shaderByteCodeSize, out IntPtr shaderText, out int shaderTextSize); + + #endregion } } diff --git a/AssetStudio/AssetsHelper.cs b/AssetStudio/AssetsHelper.cs index 844d48fc..9aa58e86 100644 --- a/AssetStudio/AssetsHelper.cs +++ b/AssetStudio/AssetsHelper.cs @@ -313,7 +313,7 @@ private static void ParseCABMap(BinaryReader reader) } } - public static async void BuildAssetMap(string[] files, string mapName, Game game, string savePath, ExportListType exportListType, ClassIDType[] typeFilters = null, Regex[] nameFilters = null, Regex[] containerFilters = null) + public static async Task BuildAssetMap(string[] files, string mapName, Game game, string savePath, ExportListType exportListType, ClassIDType[] typeFilters = null, Regex[] nameFilters = null, Regex[] containerFilters = null) { Logger.Info("Building AssetMap..."); try @@ -339,6 +339,7 @@ public static async void BuildAssetMap(string[] files, string mapName, Game game private static void BuildAssetMap(string file, List assets, ClassIDType[] typeFilters = null, Regex[] nameFilters = null, Regex[] containerFilters = null) { + var matches = new List(); var containers = new List<(PPtr, string)>(); var mihoyoBinDataNames = new List<(PPtr, string)>(); var objectAssetItemDic = new Dictionary(); @@ -403,11 +404,13 @@ private static void BuildAssetMap(string file, List assets, ClassIDT case ClassIDType.Animator when ClassIDType.Animator.CanParse(): var component = new PPtr(objectReader); animators.Add((component, asset)); + asset.Name = objectReader.type.ToString(); exportable = ClassIDType.Animator.CanExport(); break; case ClassIDType.MiHoYoBinData when ClassIDType.MiHoYoBinData.CanParse(): var MiHoYoBinData = new MiHoYoBinData(objectReader); obj = MiHoYoBinData; + asset.Name = objectReader.type.ToString(); exportable = ClassIDType.MiHoYoBinData.CanExport(); break; case ClassIDType.IndexObject when ClassIDType.IndexObject.CanParse(): @@ -455,25 +458,18 @@ private static void BuildAssetMap(string file, List assets, ClassIDT objectAssetItemDic.Add(obj, asset); 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 (exportable) { - assets.Add(asset); + matches.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))) && (typeFilters.IsNullOrEmpty() || typeFilters.Contains(asset.Type))) + if (pptr.TryGet(out var gameObject)) { asset.Name = gameObject.m_Name; } - else - { - assets.Remove(asset); - } - } foreach ((var pptr, var name) in mihoyoBinDataNames) { @@ -492,17 +488,107 @@ private static void BuildAssetMap(string file, List assets, ClassIDT { if (pptr.TryGet(out var obj)) { - var item = objectAssetItemDic[obj]; - if (containerFilters.IsNullOrEmpty() || containerFilters.Any(x => x.IsMatch(container))) + objectAssetItemDic[obj].Container = container; + } + } + + assets.AddRange(matches.Where(x => + { + var isMatchRegex = nameFilters.IsNullOrEmpty() || nameFilters.Any(y => y.IsMatch(x.Name)); + var isFilteredType = typeFilters.IsNullOrEmpty() || typeFilters.Contains(x.Type); + var isContainerMatch = containerFilters.IsNullOrEmpty() || containerFilters.Any(y => y.IsMatch(x.Container)); + return isMatchRegex && isFilteredType && isContainerMatch; + })); + } + + public static string[] ParseAssetMap(string mapName, ExportListType mapType, ClassIDType[] typeFilter, Regex[] nameFilter, Regex[] containerFilter) + { + var matches = new HashSet(); + + switch (mapType) + { + case ExportListType.MessagePack: { - item.Container = container; + using var stream = File.OpenRead(mapName); + var assetMap = MessagePackSerializer.Deserialize(stream, MessagePackSerializerOptions.Standard.WithCompression(MessagePackCompression.Lz4BlockArray)); + foreach(var entry in assetMap.AssetEntries) + { + var isNameMatch = nameFilter.Length == 0 || nameFilter.Any(x => x.IsMatch(entry.Name)); + var isContainerMatch = containerFilter.Length == 0 || containerFilter.Any(x => x.IsMatch(entry.Container)); + var isTypeMatch = typeFilter.Length == 0 || typeFilter.Any(x => x == entry.Type); + if (isNameMatch && isContainerMatch && isTypeMatch) + { + matches.Add(entry.Source); + } + } } - else + + break; + case ExportListType.XML: { - assets.Remove(item); + using var stream = File.OpenRead(mapName); + using var reader = XmlReader.Create(stream); + reader.ReadToFollowing("Assets"); + reader.ReadToFollowing("Asset"); + do + { + reader.ReadToFollowing("Name"); + var name = reader.ReadInnerXml(); + + var isNameMatch = nameFilter.Length == 0 || nameFilter.Any(x => x.IsMatch(name)); + + reader.ReadToFollowing("Container"); + var container = reader.ReadInnerXml(); + + var isContainerMatch = containerFilter.Length == 0 || containerFilter.Any(x => x.IsMatch(container)); + + reader.ReadToFollowing("Type"); + var type = reader.ReadInnerXml(); + + var isTypeMatch = typeFilter.Length == 0 || typeFilter.Any(x => x.ToString().Equals(type, StringComparison.OrdinalIgnoreCase)); + + reader.ReadToFollowing("PathID"); + var pathID = reader.ReadInnerXml(); + + reader.ReadToFollowing("Source"); + var source = reader.ReadInnerXml(); + + if (isNameMatch && isContainerMatch && isTypeMatch) + { + matches.Add(source); + } + + reader.ReadEndElement(); + } while (reader.ReadToNextSibling("Asset")); } - } + + break; + case ExportListType.JSON: + { + using var stream = File.OpenRead(mapName); + using var file = new StreamReader(stream); + using var reader = new JsonTextReader(file); + + var serializer = new JsonSerializer() { Formatting = Newtonsoft.Json.Formatting.Indented }; + serializer.Converters.Add(new StringEnumConverter()); + + var entries = serializer.Deserialize>(reader); + foreach (var entry in entries) + { + var isNameMatch = nameFilter.Length == 0 || nameFilter.Any(x => x.IsMatch(entry.Name)); + var isContainerMatch = containerFilter.Length == 0 || containerFilter.Any(x => x.IsMatch(entry.Container)); + var isTypeMatch = typeFilter.Length == 0 || typeFilter.Any(x => x == entry.Type); + if (isNameMatch && isContainerMatch && isTypeMatch) + { + matches.Add(entry.Source); + } + } + } + + break; } + + return matches.ToArray(); } private static void UpdateContainers(List assets, Game game) @@ -534,7 +620,7 @@ private static void UpdateContainers(List assets, Game game) } } - private static Task ExportAssetsMap(List toExportAssets, Game game, string name, string savePath, ExportListType exportListType, ManualResetEvent resetEvent = null) + private static Task ExportAssetsMap(List toExportAssets, Game game, string name, string savePath, ExportListType exportListType) { return Task.Run(() => { @@ -596,11 +682,9 @@ private static Task ExportAssetsMap(List toExportAssets, Game game, Logger.Info($"Finished buidling AssetMap with {toExportAssets.Count} assets."); } - - resetEvent?.Set(); }); } - public static async void BuildBoth(string[] files, string mapName, string baseFolder, Game game, string savePath, ExportListType exportListType, ClassIDType[] typeFilters = null, Regex[] nameFilters = null, Regex[] containerFilters = null) + public static async Task BuildBoth(string[] files, string mapName, string baseFolder, Game game, string savePath, ExportListType exportListType, ClassIDType[] typeFilters = null, Regex[] nameFilters = null, Regex[] containerFilters = null) { Logger.Info($"Building Both..."); CABMap.Clear(); diff --git a/AssetStudio/BlbFile.cs b/AssetStudio/BlbFile.cs index cb39e266..7bcf7083 100644 --- a/AssetStudio/BlbFile.cs +++ b/AssetStudio/BlbFile.cs @@ -8,8 +8,6 @@ namespace AssetStudio { public class BlbFile { - private const uint DefaultUncompressedSize = 0x20000; - private List m_BlocksInfo; private List m_DirectoryInfo; @@ -34,7 +32,7 @@ public BlbFile(FileReader reader, string path) version = 6, unityVersion = "5.x.x", unityRevision = "2017.4.30f1", - flags = (ArchiveFlags)0x43 + flags = 0 }; m_Header.compressedBlocksInfoSize = size; m_Header.uncompressedBlocksInfoSize = size; @@ -57,9 +55,10 @@ private void ReadBlocksInfoAndDirectory(byte[] header) var lastUncompressedSize = reader.ReadUInt32(); reader.Position += 4; - var offset = reader.ReadInt64(); + var blobOffset = reader.ReadInt32(); + var blobSize = reader.ReadUInt32(); var compressionType = (CompressionType)reader.ReadByte(); - var serializedFileVersion = (SerializedFileFormatVersion)reader.ReadByte(); + var uncompressedSize = (uint)1 << reader.ReadByte(); reader.AlignStream(); var blocksInfoCount = reader.ReadInt32(); @@ -67,7 +66,7 @@ private void ReadBlocksInfoAndDirectory(byte[] header) var blocksInfoOffset = reader.Position + reader.ReadInt64(); var nodesInfoOffset = reader.Position + reader.ReadInt64(); - var bundleInfoOffset = reader.Position + reader.ReadInt64(); + var flagInfoOffset = reader.Position + reader.ReadInt64(); reader.Position = blocksInfoOffset; m_BlocksInfo = new List(); @@ -77,8 +76,8 @@ private void ReadBlocksInfoAndDirectory(byte[] header) m_BlocksInfo.Add(new BundleFile.StorageBlock { compressedSize = reader.ReadUInt32(), - uncompressedSize = i == blocksInfoCount - 1 ? lastUncompressedSize : DefaultUncompressedSize, - flags = (StorageBlockFlags)0x43 + uncompressedSize = i == blocksInfoCount - 1 ? lastUncompressedSize : uncompressedSize, + flags = (StorageBlockFlags)compressionType }); Logger.Verbose($"Block {i} Info: {m_BlocksInfo[i]}"); @@ -95,9 +94,19 @@ private void ReadBlocksInfoAndDirectory(byte[] header) size = reader.ReadInt32() }); + var pos = reader.Position; + reader.Position = flagInfoOffset; + var flag = reader.ReadUInt32(); + if (i >= 0x20) + { + flag = reader.ReadUInt32(); + } + m_DirectoryInfo[i].flags = (uint)(flag & (1 << i)) * 4; + reader.Position = pos; + var pathOffset = reader.Position + reader.ReadInt64(); - var pos = reader.Position; + pos = reader.Position; reader.Position = pathOffset; m_DirectoryInfo[i].path = reader.ReadStringToNull(); reader.Position = pos; @@ -118,34 +127,55 @@ private Stream CreateBlocksStream(string path) return blocksStream; } - private void ReadBlocks(EndianBinaryReader reader, Stream blocksStream) + private void ReadBlocks(FileReader reader, Stream blocksStream) { foreach (var blockInfo in m_BlocksInfo) { - var compressedSize = (int)blockInfo.compressedSize; - var uncompressedSize = (int)blockInfo.uncompressedSize; - - var compressedBytes = ArrayPool.Shared.Rent(compressedSize); - var uncompressedBytes = ArrayPool.Shared.Rent(uncompressedSize); - try - { - reader.Read(compressedBytes, 0, compressedSize); - - var compressedBytesSpan = compressedBytes.AsSpan(0, compressedSize); - var uncompressedBytesSpan = uncompressedBytes.AsSpan(0, uncompressedSize); - - var numWrite = LZ4.Decompress(compressedBytesSpan, uncompressedBytesSpan); - if (numWrite != uncompressedSize) - { - throw new IOException($"Lz4 decompression error, write {numWrite} bytes but expected {uncompressedSize} bytes"); - } - - blocksStream.Write(uncompressedBytes, 0, uncompressedSize); - } - finally + var compressionType = (CompressionType)(blockInfo.flags & StorageBlockFlags.CompressionTypeMask); + Logger.Verbose($"Block compression type {compressionType}"); + switch (compressionType) //kStorageBlockCompressionTypeMask { - ArrayPool.Shared.Return(compressedBytes, true); - ArrayPool.Shared.Return(uncompressedBytes, true); + case CompressionType.None: //None + { + reader.BaseStream.CopyTo(blocksStream, blockInfo.compressedSize); + break; + } + case CompressionType.Lzma: //LZMA + { + SevenZipHelper.StreamDecompress(reader.BaseStream, blocksStream, blockInfo.compressedSize, blockInfo.uncompressedSize); + break; + } + case CompressionType.Lz4: //LZ4 + case CompressionType.Lz4HC: //LZ4HC + { + var compressedSize = (int)blockInfo.compressedSize; + var uncompressedSize = (int)blockInfo.uncompressedSize; + + var compressedBytes = ArrayPool.Shared.Rent(compressedSize); + var uncompressedBytes = ArrayPool.Shared.Rent(uncompressedSize); + + try + { + var compressedBytesSpan = compressedBytes.AsSpan(0, compressedSize); + var uncompressedBytesSpan = uncompressedBytes.AsSpan(0, uncompressedSize); + + reader.Read(compressedBytesSpan); + var numWrite = LZ4.Instance.Decompress(compressedBytesSpan, uncompressedBytesSpan); + if (numWrite != uncompressedSize) + { + throw new IOException($"Lz4 decompression error, write {numWrite} bytes but expected {uncompressedSize} bytes"); + } + blocksStream.Write(uncompressedBytesSpan); + } + finally + { + ArrayPool.Shared.Return(compressedBytes, true); + ArrayPool.Shared.Return(uncompressedBytes, true); + } + break; + } + default: + throw new IOException($"Unsupported compression type {compressionType}"); } } } diff --git a/AssetStudio/BundleFile.cs b/AssetStudio/BundleFile.cs index 5b205b1c..bbe5095a 100644 --- a/AssetStudio/BundleFile.cs +++ b/AssetStudio/BundleFile.cs @@ -37,7 +37,9 @@ public enum CompressionType Lzham, Lz4Mr0k, Lz4Inv = 5, - Zstd = 5 + Zstd = 5, + Lz4Lit4 = 4, + Lz4Lit5 = 5, } public class BundleFile @@ -359,6 +361,7 @@ private void ReadHeader(FileReader reader) m_Header.compressedBlocksInfoSize = reader.ReadUInt32(); m_Header.uncompressedBlocksInfoSize = reader.ReadUInt32(); m_Header.flags = (ArchiveFlags)reader.ReadUInt32(); + if (m_Header.signature != "UnityFS" && !Game.Type.IsSRGroup()) { reader.ReadByte(); @@ -366,8 +369,36 @@ private void ReadHeader(FileReader reader) if (Game.Type.IsNaraka()) { - m_Header.compressedBlocksInfoSize -= 0xCA; - m_Header.uncompressedBlocksInfoSize -= 0xCA; + Logger.Verbose($"Before Size: {m_Header.size} compressedBlocksInfoSize: {m_Header.compressedBlocksInfoSize} uncompressedBlocksInfoSize: {m_Header.uncompressedBlocksInfoSize} flags: {m_Header.flags}"); + long sizeOffset = m_Header.size - reader.BaseStream.Length; + Logger.Verbose($"sizeOffset: {sizeOffset}"); + switch (sizeOffset) + { + case 0x16: + m_Header.compressedBlocksInfoSize -= 0xCA; + m_Header.uncompressedBlocksInfoSize -= 0xCA; + break; + case 0x1A: + m_Header.compressedBlocksInfoSize -= 0xB4; + m_Header.uncompressedBlocksInfoSize -= 0xAA; + break; + case 0x14: + m_Header.compressedBlocksInfoSize -= 0xAA; + m_Header.uncompressedBlocksInfoSize -= 0xBE; + m_Header.flags -= 0x03; + reader.ReadUInt16(); + break; + case 0x1E: + m_Header.compressedBlocksInfoSize -= 0xF0; + m_Header.uncompressedBlocksInfoSize -= 0xE6; + m_Header.flags -= 0x03; + reader.ReadUInt16(); + break; + default: + Logger.Warning($"Unknown size offset: {sizeOffset}"); + break; + } + Logger.Verbose($"After Size: {m_Header.size} compressedBlocksInfoSize: {m_Header.compressedBlocksInfoSize} uncompressedBlocksInfoSize: {m_Header.uncompressedBlocksInfoSize} flags: {m_Header.flags}"); } Logger.Verbose($"Bundle header Info: {m_Header}"); @@ -450,7 +481,15 @@ private void ReadBlocksInfoAndDirectory(FileReader reader) try { var uncompressedBytesSpan = uncompressedBytes.AsSpan(0, (int)uncompressedSize); - var numWrite = LZ4.Decompress(blocksInfoBytesSpan, uncompressedBytesSpan); + if (Game.Type.IsPerpetualNovelty()) + { + var key = blocksInfoBytesSpan[1]; + for (int j = 0; j < Math.Min(0x32, blocksInfoBytesSpan.Length); j++) + { + blocksInfoBytesSpan[j] ^= key; + } + } + var numWrite = LZ4.Instance.Decompress(blocksInfoBytesSpan, uncompressedBytesSpan); if (numWrite != uncompressedSize) { throw new IOException($"Lz4 decompression error, write {numWrite} bytes but expected {uncompressedSize} bytes"); @@ -484,11 +523,18 @@ private void ReadBlocksInfoAndDirectory(FileReader reader) Logger.Verbose($"Blocks count: {blocksInfoCount}"); for (int i = 0; i < blocksInfoCount; i++) { + UInt32 blockUncompressedSize = blocksInfoReader.ReadUInt32(); + UInt32 blockCompressedSize = blocksInfoReader.ReadUInt32(); + UInt16 blockFlags = blocksInfoReader.ReadUInt16(); + if (Game.Type.IsNaraka() && blockFlags == 0x06) + { + blockFlags -= 0x03; + } m_BlocksInfo.Add(new StorageBlock { - uncompressedSize = blocksInfoReader.ReadUInt32(), - compressedSize = blocksInfoReader.ReadUInt32(), - flags = (StorageBlockFlags)blocksInfoReader.ReadUInt16() + uncompressedSize = blockUncompressedSize, + compressedSize = blockCompressedSize, + flags = (StorageBlockFlags)blockFlags }); Logger.Verbose($"Block {i} Info: {m_BlocksInfo[i]}"); @@ -584,7 +630,7 @@ private void ReadBlocks(FileReader reader, Stream blocksStream) { OPFPUtils.Decrypt(compressedBytesSpan, reader.FullPath); } - var numWrite = LZ4.Decompress(compressedBytesSpan, uncompressedBytesSpan); + var numWrite = LZ4.Instance.Decompress(compressedBytesSpan, uncompressedBytesSpan); if (numWrite != uncompressedSize) { throw new IOException($"Lz4 decompression error, write {numWrite} bytes but expected {uncompressedSize} bytes"); @@ -615,9 +661,37 @@ private void ReadBlocks(FileReader reader, Stream blocksStream) if (i == 0) { FairGuardUtils.Decrypt(compressedBytesSpan); + } + var numWrite = LZ4Inv.Instance.Decompress(compressedBytesSpan, uncompressedBytesSpan); + if (numWrite != uncompressedSize) + { + throw new IOException($"Lz4 decompression error, write {numWrite} bytes but expected {uncompressedSize} bytes"); } - var numWrite = LZ4Inv.Decompress(compressedBytesSpan, uncompressedBytesSpan); + blocksStream.Write(uncompressedBytesSpan); + } + finally + { + ArrayPool.Shared.Return(compressedBytes, true); + ArrayPool.Shared.Return(uncompressedBytes, true); + } + break; + } + case CompressionType.Lz4Lit4 or CompressionType.Lz4Lit5 when Game.Type.IsExAstris(): + { + var compressedSize = (int)blockInfo.compressedSize; + var uncompressedSize = (int)blockInfo.uncompressedSize; + + var compressedBytes = ArrayPool.Shared.Rent(compressedSize); + var uncompressedBytes = ArrayPool.Shared.Rent(uncompressedSize); + + var compressedBytesSpan = compressedBytes.AsSpan(0, compressedSize); + var uncompressedBytesSpan = uncompressedBytes.AsSpan(0, uncompressedSize); + + try + { + reader.Read(compressedBytesSpan); + var numWrite = LZ4Lit.Instance.Decompress(compressedBytesSpan, uncompressedBytesSpan); if (numWrite != uncompressedSize) { throw new IOException($"Lz4 decompression error, write {numWrite} bytes but expected {uncompressedSize} bytes"); diff --git a/AssetStudio/Classes/AnimationClip.cs b/AssetStudio/Classes/AnimationClip.cs index 34894a13..2cf66857 100644 --- a/AssetStudio/Classes/AnimationClip.cs +++ b/AssetStudio/Classes/AnimationClip.cs @@ -762,8 +762,8 @@ public HumanPose(ObjectReader reader) if (version[0] > 5 || (version[0] == 5 && version[1] >= 2))//5.2 and up { m_TDoFArray = reader.ReadVector3Array(); - } } + } public static HumanPose ParseGI(ObjectReader reader) { @@ -1118,7 +1118,7 @@ public static DenseClip ParseGI(ObjectReader reader) return denseClip; } } - public class ArkDenseClip : DenseClip + public class ACLDenseClip : DenseClip { public int m_ACLType; public byte[] m_ACLArray; @@ -1130,21 +1130,38 @@ public class ArkDenseClip : DenseClip public uint m_nRotationCurves; public uint m_nEulerCurves; public uint m_nScaleCurves; + public uint m_nGenericCurves; - public ArkDenseClip(ObjectReader reader) : base(reader) + public ACLDenseClip(ObjectReader reader) : base(reader) { m_ACLType = reader.ReadInt32(); - m_ACLArray = reader.ReadUInt8Array(); - reader.AlignStream(); - m_PositionFactor = reader.ReadSingle(); - m_EulerFactor = reader.ReadSingle(); - m_ScaleFactor = reader.ReadSingle(); - m_FloatFactor = reader.ReadSingle(); - m_nPositionCurves = reader.ReadUInt32(); - m_nRotationCurves = reader.ReadUInt32(); - m_nEulerCurves = reader.ReadUInt32(); - m_nScaleCurves = reader.ReadUInt32(); - + if (reader.Game.Type.IsArknightsEndfield()) + { + m_ACLArray = reader.ReadUInt8Array(); + reader.AlignStream(); + m_PositionFactor = reader.ReadSingle(); + m_EulerFactor = reader.ReadSingle(); + m_ScaleFactor = reader.ReadSingle(); + m_FloatFactor = reader.ReadSingle(); + m_nPositionCurves = reader.ReadUInt32(); + m_nRotationCurves = reader.ReadUInt32(); + m_nEulerCurves = reader.ReadUInt32(); + m_nScaleCurves = reader.ReadUInt32(); + } + else if (reader.Game.Type.IsExAstris()) + { + m_nPositionCurves = reader.ReadUInt32(); + m_nRotationCurves = reader.ReadUInt32(); + m_nEulerCurves = reader.ReadUInt32(); + m_nScaleCurves = reader.ReadUInt32(); + m_nGenericCurves = reader.ReadUInt32(); + m_PositionFactor = reader.ReadSingle(); + m_EulerFactor = reader.ReadSingle(); + m_ScaleFactor = reader.ReadSingle(); + m_FloatFactor = reader.ReadSingle(); + m_ACLArray = reader.ReadUInt8Array(); + reader.AlignStream(); + } Process(); } @@ -1181,7 +1198,7 @@ private void Process() { sampleArray.Add(ReadCurve(aclSpan, m_ScaleFactor, ref index)); } - var m_nFloatCurves = m_CurveCount - (m_nPositionCurves + m_nRotationCurves + m_nEulerCurves + m_nScaleCurves); + var m_nFloatCurves = m_CurveCount - (m_nPositionCurves + m_nRotationCurves + m_nEulerCurves + m_nScaleCurves + m_nGenericCurves); for (int j = 0; j < m_nFloatCurves; j++) { sampleArray.Add(ReadCurve(aclSpan, m_FloatFactor, ref index)); @@ -1299,9 +1316,9 @@ public Clip(ObjectReader reader) { var version = reader.version; m_StreamedClip = new StreamedClip(reader); - if (reader.Game.Type.IsArknightsEndfield()) + if (reader.Game.Type.IsArknightsEndfield() || reader.Game.Type.IsExAstris()) { - m_DenseClip = new ArkDenseClip(reader); + m_DenseClip = new ACLDenseClip(reader); } else { @@ -1866,6 +1883,11 @@ public AnimationClip(ObjectReader reader) : base(reader) m_CompressedRotationCurves.Add(new CompressedAnimationCurve(reader)); } + if (reader.Game.Type.IsExAstris()) + { + var m_aclType = reader.ReadInt32(); + } + if (version[0] > 5 || (version[0] == 5 && version[1] >= 3))//5.3 and up { int numEulerCurves = reader.ReadInt32(); diff --git a/AssetStudio/Classes/GameObject.cs b/AssetStudio/Classes/GameObject.cs index 18903b3d..4ff12e7f 100644 --- a/AssetStudio/Classes/GameObject.cs +++ b/AssetStudio/Classes/GameObject.cs @@ -40,27 +40,35 @@ public GameObject(ObjectReader reader) : base(reader) public bool HasModel() => HasMesh(m_Transform, new List()); private static bool HasMesh(Transform m_Transform, List meshes) { - m_Transform.m_GameObject.TryGet(out var m_GameObject); - - if (m_GameObject.m_MeshRenderer != null) + try { - var mesh = GetMesh(m_GameObject.m_MeshRenderer); - meshes.Add(mesh != null); - } + m_Transform.m_GameObject.TryGet(out var m_GameObject); - if (m_GameObject.m_SkinnedMeshRenderer != null) - { - var mesh = GetMesh(m_GameObject.m_SkinnedMeshRenderer); - meshes.Add(mesh != null); - } + if (m_GameObject.m_MeshRenderer != null) + { + var mesh = GetMesh(m_GameObject.m_MeshRenderer); + meshes.Add(mesh != null); + } + + if (m_GameObject.m_SkinnedMeshRenderer != null) + { + var mesh = GetMesh(m_GameObject.m_SkinnedMeshRenderer); + meshes.Add(mesh != null); + } - foreach (var pptr in m_Transform.m_Children) + foreach (var pptr in m_Transform.m_Children) + { + if (pptr.TryGet(out var child)) + meshes.Add(HasMesh(child, meshes)); + } + + return meshes.Any(x => x == true); + } + catch(Exception e) { - if (pptr.TryGet(out var child)) - meshes.Add(HasMesh(child, meshes)); + Logger.Warning($"Unable to verify if {m_Transform?.Name} has meshes, skipping..."); + return false; } - - return meshes.Any(x => x == true); } private static Mesh GetMesh(Renderer meshR) diff --git a/AssetStudio/Classes/Mesh.cs b/AssetStudio/Classes/Mesh.cs index 7ab0ba38..d2fb62ba 100644 --- a/AssetStudio/Classes/Mesh.cs +++ b/AssetStudio/Classes/Mesh.cs @@ -155,7 +155,7 @@ public VertexData(ObjectReader reader) m_Streams = new List(); for (int i = 0; i < numStreams; i++) { - m_Streams[i] = new StreamInfo(reader); + m_Streams.Add(new StreamInfo(reader)); } if (version[0] < 4) //4.0 down @@ -706,6 +706,11 @@ public Mesh(ObjectReader reader) : base(reader) int m_CollisionVertexCount = reader.ReadInt32(); } + if (reader.Game.Type.IsExAstris()) + { + var m_ColliderType = reader.ReadInt32(); + } + int m_MeshUsageFlags = reader.ReadInt32(); if (version[0] > 2022 || (version[0] == 2022 && version[1] >= 1)) //2022.1 and up diff --git a/AssetStudio/Classes/Shader.cs b/AssetStudio/Classes/Shader.cs index 9ad7b0fd..240dcf19 100644 --- a/AssetStudio/Classes/Shader.cs +++ b/AssetStudio/Classes/Shader.cs @@ -89,13 +89,27 @@ public enum SerializedPropertyType Int = 5 }; + [Flags] + public enum SerializedPropertyFlag + { + HideInInspector = 1 << 0, + PerRendererData = 1 << 1, + NoScaleOffset = 1 << 2, + Normal = 1 << 3, + HDR = 1 << 4, + Gamma = 1 << 5, + NonModifiableTextureData = 1 << 6, + MainTexture = 1 << 7, + MainColor = 1 << 8, + } + public class SerializedProperty { public string m_Name; public string m_Description; public string[] m_Attributes; public SerializedPropertyType m_Type; - public uint m_Flags; + public SerializedPropertyFlag m_Flags; public float[] m_DefValue; public SerializedTextureProperty m_DefTexture; @@ -105,7 +119,7 @@ public SerializedProperty(EndianBinaryReader reader) m_Description = reader.ReadAlignedString(); m_Attributes = reader.ReadStringArray(); m_Type = (SerializedPropertyType)reader.ReadInt32(); - m_Flags = reader.ReadUInt32(); + m_Flags = (SerializedPropertyFlag)reader.ReadUInt32(); m_DefValue = reader.ReadSingleArray(4); m_DefTexture = new SerializedTextureProperty(reader); } diff --git a/AssetStudio/Classes/Texture2D.cs b/AssetStudio/Classes/Texture2D.cs index b4ea00b6..3dc23c41 100644 --- a/AssetStudio/Classes/Texture2D.cs +++ b/AssetStudio/Classes/Texture2D.cs @@ -39,6 +39,10 @@ public GLTextureSettings(ObjectReader reader) m_FilterMode = reader.ReadInt32(); m_Aniso = reader.ReadInt32(); m_MipBias = reader.ReadSingle(); + if (reader.Game.Type.IsExAstris()) + { + var m_TextureGroup = reader.ReadInt32(); + } if (version[0] >= 2017)//2017.x and up { m_WrapMode = reader.ReadInt32(); //m_WrapU diff --git a/AssetStudio/EndianBinaryReader.cs b/AssetStudio/EndianBinaryReader.cs index 94541e60..7f926411 100644 --- a/AssetStudio/EndianBinaryReader.cs +++ b/AssetStudio/EndianBinaryReader.cs @@ -252,108 +252,108 @@ internal T[] ReadArray(Func del, int length) } } - public bool[] ReadBooleanArray(int length = 0) + public bool[] ReadBooleanArray(int length = -1) { - if (length == 0) + if (length == -1) { length = ReadInt32(); } return ReadArray(ReadBoolean, length); } - public byte[] ReadUInt8Array(int length = 0) + public byte[] ReadUInt8Array(int length = -1) { - if (length == 0) + if (length == -1) { length = ReadInt32(); } return ReadBytes(length); } - public short[] ReadInt16Array(int length = 0) + public short[] ReadInt16Array(int length = -1) { - if (length == 0) + if (length == -1) { length = ReadInt32(); } return ReadArray(ReadInt16, length); } - public ushort[] ReadUInt16Array(int length = 0) + public ushort[] ReadUInt16Array(int length = -1) { - if (length == 0) + if (length == -1) { length = ReadInt32(); } return ReadArray(ReadUInt16, length); } - public int[] ReadInt32Array(int length = 0) + public int[] ReadInt32Array(int length = -1) { - if (length == 0) + if (length == -1) { length = ReadInt32(); } return ReadArray(ReadInt32, length); } - public uint[] ReadUInt32Array(int length = 0) + public uint[] ReadUInt32Array(int length = -1) { - if (length == 0) + if (length == -1) { length = ReadInt32(); } return ReadArray(ReadUInt32, length); } - public uint[][] ReadUInt32ArrayArray(int length = 0) + public uint[][] ReadUInt32ArrayArray(int length = -1) { - if (length == 0) + if (length == -1) { length = ReadInt32(); } return ReadArray(() => ReadUInt32Array(), length); } - public float[] ReadSingleArray(int length = 0) + public float[] ReadSingleArray(int length = -1) { - if (length == 0) + if (length == -1) { length = ReadInt32(); } return ReadArray(ReadSingle, length); } - public string[] ReadStringArray(int length = 0) + public string[] ReadStringArray(int length = -1) { - if (length == 0) + if (length == -1) { length = ReadInt32(); } return ReadArray(ReadAlignedString, length); } - public Vector2[] ReadVector2Array(int length = 0) + public Vector2[] ReadVector2Array(int length = -1) { - if (length == 0) + if (length == -1) { length = ReadInt32(); } return ReadArray(ReadVector2, length); } - public Vector4[] ReadVector4Array(int length = 0) + public Vector4[] ReadVector4Array(int length = -1) { - if (length == 0) + if (length == -1) { length = ReadInt32(); } return ReadArray(ReadVector4, length); } - public Matrix4x4[] ReadMatrixArray(int length = 0) + public Matrix4x4[] ReadMatrixArray(int length = -1) { - if (length == 0) + if (length == -1) { length = ReadInt32(); } diff --git a/AssetStudio/GameManager.cs b/AssetStudio/GameManager.cs index 48cf9ba7..bf595be6 100644 --- a/AssetStudio/GameManager.cs +++ b/AssetStudio/GameManager.cs @@ -48,6 +48,8 @@ static GameManager() Games.Add(index++, new Game(GameType.PartyAnimals)); Games.Add(index++, new Game(GameType.LoveAndDeepspace)); Games.Add(index++, new Game(GameType.SchoolGirlStrikers)); + Games.Add(index++, new Game(GameType.ExAstris)); + Games.Add(index++, new Game(GameType.PerpetualNovelty)); } public static Game GetGame(GameType gameType) => GetGame((int)gameType); public static Game GetGame(int index) @@ -167,6 +169,8 @@ public enum GameType PartyAnimals, LoveAndDeepspace, SchoolGirlStrikers, + ExAstris, + PerpetualNovelty, } public static class GameTypes @@ -191,6 +195,8 @@ public static class GameTypes public static bool IsNetEase(this GameType type) => type == GameType.NetEase; public static bool IsArknightsEndfield(this GameType type) => type == GameType.ArknightsEndfield; public static bool IsLoveAndDeepspace(this GameType type) => type == GameType.LoveAndDeepspace; + public static bool IsExAstris(this GameType type) => type == GameType.ExAstris; + public static bool IsPerpetualNovelty(this GameType type) => type == GameType.PerpetualNovelty; public static bool IsGIGroup(this GameType type) => type switch { GameType.GI or GameType.GI_Pack or GameType.GI_CB1 or GameType.GI_CB2 or GameType.GI_CB3 or GameType.GI_CB3Pre => true, diff --git a/AssetStudio/ImportHelper.cs b/AssetStudio/ImportHelper.cs index 1ebee94f..10580419 100644 --- a/AssetStudio/ImportHelper.cs +++ b/AssetStudio/ImportHelper.cs @@ -277,11 +277,6 @@ public static FileReader DecryptMark(FileReader reader) public static FileReader DecryptEnsembleStar(FileReader reader) { Logger.Verbose($"Attempting to decrypt file {reader.FileName} with Ensemble Star encryption"); - if (Path.GetExtension(reader.FileName) != ".z") - { - Logger.Verbose($"Expected file extension .z, found {Path.GetExtension(reader.FileName)} instead, aborting..."); - return reader; - } using (reader) { var data = reader.ReadBytes((int)reader.Length); diff --git a/AssetStudio/LZ4/LZ4.cs b/AssetStudio/LZ4/LZ4.cs new file mode 100644 index 00000000..3ee6af7b --- /dev/null +++ b/AssetStudio/LZ4/LZ4.cs @@ -0,0 +1,70 @@ +using System; + +namespace AssetStudio; +public class LZ4 +{ + public static LZ4 Instance => new(); + public virtual int Decompress(ReadOnlySpan cmp, Span dec) + { + int cmpPos = 0; + int decPos = 0; + + do + { + var (encCount, litCount) = GetLiteralToken(cmp, ref cmpPos); + + //Copy literal chunk + litCount = GetLength(litCount, cmp, ref cmpPos); + + cmp.Slice(cmpPos, litCount).CopyTo(dec.Slice(decPos)); + + cmpPos += litCount; + decPos += litCount; + + if (cmpPos >= cmp.Length) + { + break; + } + + //Copy compressed chunk + int back = GetChunkEnd(cmp, ref cmpPos); + + encCount = GetLength(encCount, cmp, ref cmpPos) + 4; + + int encPos = decPos - back; + + if (encCount <= back) + { + dec.Slice(encPos, encCount).CopyTo(dec.Slice(decPos)); + + decPos += encCount; + } + else + { + while (encCount-- > 0) + { + dec[decPos++] = dec[encPos++]; + } + } + } while (cmpPos < cmp.Length && + decPos < dec.Length); + + return decPos; + } + protected virtual (int encCount, int litCount) GetLiteralToken(ReadOnlySpan cmp, ref int cmpPos) => ((cmp[cmpPos] >> 0) & 0xf, (cmp[cmpPos++] >> 4) & 0xf); + protected virtual int GetChunkEnd(ReadOnlySpan cmp, ref int cmpPos) => cmp[cmpPos++] << 0 | cmp[cmpPos++] << 8; + protected virtual int GetLength(int length, ReadOnlySpan cmp, ref int cmpPos) + { + byte sum; + + if (length == 0xf) + { + do + { + length += sum = cmp[cmpPos++]; + } while (sum == 0xff); + } + + return length; + } +} \ No newline at end of file diff --git a/AssetStudio/LZ4/LZ4Inv.cs b/AssetStudio/LZ4/LZ4Inv.cs new file mode 100644 index 00000000..042fea22 --- /dev/null +++ b/AssetStudio/LZ4/LZ4Inv.cs @@ -0,0 +1,9 @@ +using System; + +namespace AssetStudio; +public class LZ4Inv : LZ4 +{ + public new static LZ4Inv Instance => new(); + protected override (int encCount, int litCount) GetLiteralToken(ReadOnlySpan cmp, ref int cmpPos) => ((cmp[cmpPos] >> 4) & 0xf, (cmp[cmpPos++] >> 0) & 0xf); + protected override int GetChunkEnd(ReadOnlySpan cmp, ref int cmpPos) => cmp[cmpPos++] << 8 | cmp[cmpPos++] << 0; +} \ No newline at end of file diff --git a/AssetStudio/LZ4/LZ4Lit.cs b/AssetStudio/LZ4/LZ4Lit.cs new file mode 100644 index 00000000..a8a0aee7 --- /dev/null +++ b/AssetStudio/LZ4/LZ4Lit.cs @@ -0,0 +1,8 @@ +using System; + +namespace AssetStudio; +public class LZ4Lit : LZ4 +{ + public new static LZ4Lit Instance => new(); + protected override (int encCount, int litCount) GetLiteralToken(ReadOnlySpan cmp, ref int cmpPos) => ((cmp[cmpPos] >> 4) & 0xf, (cmp[cmpPos++] >> 0) & 0xf); +} \ No newline at end of file diff --git a/AssetStudio/LZ4/LZ4Utils.cs b/AssetStudio/LZ4/LZ4Utils.cs deleted file mode 100644 index 0f22898a..00000000 --- a/AssetStudio/LZ4/LZ4Utils.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System; - -namespace AssetStudio; -public static class LZ4 -{ - public static int Decompress(ReadOnlySpan cmp, Span dec) - { - int cmpPos = 0; - int decPos = 0; - - // ReSharper disable once VariableHidesOuterVariable - int GetLength(int length, ReadOnlySpan cmp) - { - byte sum; - - if (length == 0xf) - { - do - { - length += sum = cmp[cmpPos++]; - } while (sum == 0xff); - } - - return length; - } - - do - { - byte token = cmp[cmpPos++]; - - int encCount = (token >> 0) & 0xf; - int litCount = (token >> 4) & 0xf; - - //Copy literal chunk - litCount = GetLength(litCount, cmp); - - cmp.Slice(cmpPos, litCount).CopyTo(dec.Slice(decPos)); - - cmpPos += litCount; - decPos += litCount; - - if (cmpPos >= cmp.Length) - { - break; - } - - //Copy compressed chunk - int back = cmp[cmpPos++] << 0 | - cmp[cmpPos++] << 8; - - encCount = GetLength(encCount, cmp) + 4; - - int encPos = decPos - back; - - if (encCount <= back) - { - dec.Slice(encPos, encCount).CopyTo(dec.Slice(decPos)); - - decPos += encCount; - } - else - { - while (encCount-- > 0) - { - dec[decPos++] = dec[encPos++]; - } - } - } while (cmpPos < cmp.Length && - decPos < dec.Length); - - return decPos; - } -} -public static class LZ4Inv -{ - public static int Decompress(ReadOnlySpan cmp, Span dec) - { - int cmpPos = 0; - int decPos = 0; - - // ReSharper disable once VariableHidesOuterVariable - int GetLength(int length, ReadOnlySpan cmp) - { - byte sum; - - if (length == 0xf) - { - do - { - length += sum = cmp[cmpPos++]; - } while (sum == 0xff); - } - - return length; - } - - do - { - byte token = cmp[cmpPos++]; - - int encCount = (token >> 4) & 0xf; - int litCount = (token >> 0) & 0xf; - - //Copy literal chunk - litCount = GetLength(litCount, cmp); - - cmp.Slice(cmpPos, litCount).CopyTo(dec.Slice(decPos)); - - cmpPos += litCount; - decPos += litCount; - - if (cmpPos >= cmp.Length) - { - break; - } - - //Copy compressed chunk - int back = cmp[cmpPos++] << 8 | - cmp[cmpPos++] << 0; - - encCount = GetLength(encCount, cmp) + 4; - - int encPos = decPos - back; - - if (encCount <= back) - { - dec.Slice(encPos, encCount).CopyTo(dec.Slice(decPos)); - - decPos += encCount; - } - else - { - while (encCount-- > 0) - { - dec[decPos++] = dec[encPos++]; - } - } - } while (cmpPos < cmp.Length && - decPos < dec.Length); - - return decPos; - } -} diff --git a/AssetStudio/MhyFile.cs b/AssetStudio/MhyFile.cs index 6e0e3677..a11a3c8e 100644 --- a/AssetStudio/MhyFile.cs +++ b/AssetStudio/MhyFile.cs @@ -63,7 +63,7 @@ private void ReadBlocksInfoAndDirectory(FileReader reader) try { - var numWrite = LZ4.Decompress(compressedBlocksInfo, uncompressedBlocksInfoSpan); + var numWrite = LZ4.Instance.Decompress(compressedBlocksInfo, uncompressedBlocksInfoSpan); if (numWrite != m_Header.uncompressedBlocksInfoSize) { throw new IOException($"Lz4 decompression error, write {numWrite} bytes but expected {m_Header.uncompressedBlocksInfoSize} bytes"); @@ -144,7 +144,7 @@ private void ReadBlocks(EndianBinaryReader reader, Stream blocksStream) DescrambleEntry(compressedBytesSpan); Logger.Verbose($"Descrambled block signature {Convert.ToHexString(compressedBytes, 0, 4)}"); - var numWrite = LZ4.Decompress(compressedBytesSpan[0xC..], uncompressedBytesSpan); + var numWrite = LZ4.Instance.Decompress(compressedBytesSpan[0xC..], uncompressedBytesSpan); if (numWrite != uncompressedSize) { throw new IOException($"Lz4 decompression error, write {numWrite} bytes but expected {uncompressedSize} bytes"); diff --git a/AssetStudio/TypeFlags.cs b/AssetStudio/TypeFlags.cs index fda55ac9..8d685983 100644 --- a/AssetStudio/TypeFlags.cs +++ b/AssetStudio/TypeFlags.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace AssetStudio; public static class TypeFlags @@ -44,3 +45,12 @@ public static bool CanExport(this ClassIDType type) return false; } } + +[Flags] +public enum TypeFlag +{ + None, + Parse, + Export, + Both = Parse | Export, +} \ No newline at end of file