From 3d1799b9df585d6fca426c0f3953a22c7f7ad4a3 Mon Sep 17 00:00:00 2001 From: Razmoth <12517189-Razmoth@users.noreply.gitlab.com> Date: Thu, 18 May 2023 20:45:30 +0400 Subject: [PATCH] - `AssetBrowser` Optimizations. - Filter options added to `AssetBrowser`. - New game entry. - Togglable Model Preview. - bug fixes. --- AssetStudio/AssetMap.cs | 24 +- AssetStudio/BundleFile.cs | 4 + AssetStudio/Classes/GameObject.cs | 54 +++- AssetStudio/Crypto/NetEaseUtils.cs | 243 ++++++++++++++++++ AssetStudio/Extensions/ByteArrayExtensions.cs | 7 +- AssetStudio/GameManager.cs | 5 +- AssetStudioCLI/Components/CommandLine.cs | 2 +- AssetStudioGUI/App.config | 3 + AssetStudioGUI/AssetBrowser.Designer.cs | 47 ++-- AssetStudioGUI/AssetBrowser.cs | 46 +++- AssetStudioGUI/AssetStudioGUIForm.Designer.cs | 56 ++-- AssetStudioGUI/AssetStudioGUIForm.cs | 36 ++- AssetStudioGUI/AssetStudioGUIForm.resx | 6 +- .../Properties/Settings.Designer.cs | 12 + AssetStudioGUI/Properties/Settings.settings | 3 + 15 files changed, 477 insertions(+), 71 deletions(-) create mode 100644 AssetStudio/Crypto/NetEaseUtils.cs diff --git a/AssetStudio/AssetMap.cs b/AssetStudio/AssetMap.cs index b0d750a..97142b8 100644 --- a/AssetStudio/AssetMap.cs +++ b/AssetStudio/AssetMap.cs @@ -1,5 +1,7 @@ using MessagePack; +using System; using System.Collections.Generic; +using System.Linq; using System.Text.RegularExpressions; namespace AssetStudio @@ -26,10 +28,22 @@ namespace AssetStudio [Key(4)] public ClassIDType Type { get; set; } - public bool Matches(Regex regex) => regex.IsMatch(Name) - || regex.IsMatch(Container) - || regex.IsMatch(Source) - || regex.IsMatch(PathID.ToString()) - || regex.IsMatch(Type.ToString()); + public bool Matches(Dictionary filters) + { + var matches = new List(); + foreach(var filter in filters) + { + matches.Add(filter.Key switch + { + string value when value.Equals(nameof(Name), StringComparison.OrdinalIgnoreCase) => filter.Value.IsMatch(Name), + string value when value.Equals(nameof(Container), StringComparison.OrdinalIgnoreCase) => filter.Value.IsMatch(Container), + string value when value.Equals(nameof(Source), StringComparison.OrdinalIgnoreCase) => filter.Value.IsMatch(Source), + string value when value.Equals(nameof(PathID), StringComparison.OrdinalIgnoreCase) => filter.Value.IsMatch(PathID.ToString()), + string value when value.Equals(nameof (Type), StringComparison.OrdinalIgnoreCase) => filter.Value.IsMatch(Type.ToString()), + _ => throw new NotImplementedException() + }); + } + return matches.Count(x => x == true) == filters.Count; + } } } diff --git a/AssetStudio/BundleFile.cs b/AssetStudio/BundleFile.cs index 279d669..9f48363 100644 --- a/AssetStudio/BundleFile.cs +++ b/AssetStudio/BundleFile.cs @@ -437,6 +437,10 @@ namespace AssetStudio { OPFPUtils.Decrypt(compressedBytesSpan, reader.FullPath); } + if (Game.Type.IsNetEase() && i == 0) + { + NetEaseUtils.Decrypt(compressedBytesSpan); + } var uncompressedSize = (int)blockInfo.uncompressedSize; var uncompressedBytes = BigArrayPool.Shared.Rent(uncompressedSize); var uncompressedBytesSpan = uncompressedBytes.AsSpan(0, uncompressedSize); diff --git a/AssetStudio/Classes/GameObject.cs b/AssetStudio/Classes/GameObject.cs index 0196118..48bef0b 100644 --- a/AssetStudio/Classes/GameObject.cs +++ b/AssetStudio/Classes/GameObject.cs @@ -1,5 +1,7 @@ -using System; +using AssetStudio; +using System; using System.Collections.Generic; +using System.Diagnostics.Metrics; using System.Linq; using System.Text; @@ -34,6 +36,54 @@ namespace AssetStudio m_Name = reader.ReadAlignedString(); } - public bool HasModel() => m_Transform != null && m_Transform.m_Father.IsNull && m_Transform.m_Children.Length > 0; + 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) + { + 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) + { + if (pptr.TryGet(out var child)) + meshes.Add(HasMesh(child, meshes)); + } + + return meshes.Any(x => x == true); + } + + private static Mesh GetMesh(Renderer meshR) + { + if (meshR is SkinnedMeshRenderer sMesh) + { + if (sMesh.m_Mesh.TryGet(out var m_Mesh)) + { + return m_Mesh; + } + } + else + { + meshR.m_GameObject.TryGet(out var m_GameObject); + if (m_GameObject.m_MeshFilter != null) + { + if (m_GameObject.m_MeshFilter.m_Mesh.TryGet(out var m_Mesh)) + { + return m_Mesh; + } + } + } + + return null; + } } } diff --git a/AssetStudio/Crypto/NetEaseUtils.cs b/AssetStudio/Crypto/NetEaseUtils.cs new file mode 100644 index 0000000..d56ce3c --- /dev/null +++ b/AssetStudio/Crypto/NetEaseUtils.cs @@ -0,0 +1,243 @@ +using System; +using System.Buffers.Binary; +using System.Runtime.InteropServices; +using System.Text; + +namespace AssetStudio +{ + //Special thanks to LukeFZ#4035. + public static class NetEaseUtils + { + private static readonly byte[] Signature = new byte[] { 0xEE, 0xDD }; + public static void Decrypt(Span bytes) + { + var (encryptedOffset, encryptedSize) = ReadHeader(bytes); + var encrypted = bytes.Slice(encryptedOffset, encryptedSize); + var encryptedInts = MemoryMarshal.Cast(encrypted); + + var seedInts = new int[] { encryptedInts[3], encryptedInts[1], encryptedInts[4], encrypted.Length, encryptedInts[2] }; + var seedBytes = MemoryMarshal.AsBytes(seedInts).ToArray(); + var seed = (int)CRC.CalculateDigest(seedBytes, 0, (uint)seedBytes.Length); + + var keyPart0 = seed ^ (encryptedInts[7] + 0x1981); + var keyPart1 = seed ^ (encrypted.Length + 0x2013); + var keyPart2 = seed ^ (encryptedInts[5] + 0x1985); + var keyPart3 = seed ^ (encryptedInts[6] + 0x2018); + + for (int i = 0; i < 0x20; i++) + { + encrypted[i] ^= 0xA6; + } + + var block = encrypted[0x20..]; + var keyVector = new int[] { keyPart2, keyPart0, keyPart1, keyPart3 }; + var keysVector = new int[] { 0x571, keyPart3, 0x892, 0x750, keyPart2, keyPart0, 0x746, keyPart1, 0x568 }; + if (block.Length >= 0x80) + { + var dataBlock = block[0x80..]; + var keyBlock = block[..0x80].ToArray(); + var keyBlockInts = MemoryMarshal.Cast(keyBlock); + + RC4(block[..0x80], seed); + RC4(keyBlock, keyPart1); + + var blockCount = dataBlock.Length / 0x80; + for (int i = 0; i < blockCount; i++) + { + var blockOffset = i * 0x80; + var type = (byte)keysVector[i % keysVector.Length] % keyVector.Length; + var dataBlockInts = MemoryMarshal.Cast(dataBlock.Slice(blockOffset, 0x80)); + for (int j = 0; j < 0x20; j++) + { + dataBlockInts[j] ^= keyBlockInts[j] ^ type switch + { + 0 => keysVector[j % keysVector.Length] ^ (0x20 - j), + 1 => keyVector[(byte)keyBlockInts[j] % keyVector.Length], + 2 => keyVector[(byte)keyBlockInts[j] % keyVector.Length] ^ j, + 3 => keyVector[(byte)keysVector[j % keysVector.Length] % keyVector.Length] ^ j, + _ => throw new NotImplementedException() + }; + } + } + + var remainingCount = dataBlock.Length % 0x80; + if (remainingCount > 0) + { + var remaining = encrypted[^remainingCount..]; + for (int i = 0; i < remainingCount; i++) + { + remaining[i] ^= (byte)(keyBlock[i] ^ ((uint)keysVector[(uint)keyVector[i % keyVector.Length] % keysVector.Length] % 0xFF) ^ i); + } + } + } + else + { + RC4(block, seed); + } + } + private static (int, int) ReadHeader(Span bytes) + { + var index = bytes.Search(Signature, 0); + if (index == -1 || index >= 0x40) + { + throw new Exception("Header not found !!"); + } + + var info = bytes[index..]; + ReadVersion(info); + ReadEncryptedSize(info, bytes.Length, out var encryptedSize); + + var headerOffset = 0; + ReadHeaderOffset(info, 8, ref headerOffset); + ReadHeaderOffset(info, 9, ref headerOffset); + + var headerSize = 0x30; + var encryptedOffset = 0x30; + if (headerOffset == index || headerOffset == 0) + { + if (index >= 0x20) + { + headerSize = 0x40; + encryptedOffset = 0x40; + } + } + else + { + if (headerOffset >= 0x20) + { + headerSize = 0x40; + encryptedOffset = 0x40; + } + if (headerOffset > index) + { + encryptedOffset += index - headerOffset; + } + } + + encryptedSize -= headerSize; + + return (encryptedOffset, encryptedSize); + } + + private static void ReadVersion(Span bytes) + { + var version = BinaryPrimitives.ReadUInt16LittleEndian(bytes[2..]); + if (version < 0x2017 || version > 0x2025) + { + throw new Exception("Unsupported version"); + } + var versionString = version.ToString("X4"); + Encoding.UTF8.GetBytes(versionString, bytes); + } + + private static void ReadEncryptedSize(Span bytes, int size, out int encryptedSize) + { + var (vectorCount, bytesCount) = (bytes[4], bytes[6]); + encryptedSize = size > 0x1000 ? 0x1000 : size; + if (vectorCount != 0x2E && bytesCount != 0x2E) + { + encryptedSize = bytesCount + 0x10 * vectorCount; + if (vectorCount == 0xAA && bytesCount == 0xBB) + { + encryptedSize = 0x1000; + } + bytes[4] = bytes[6] = 0x2E; + } + } + + private static void ReadHeaderOffset(Span bytes, int index, ref int headerOffset) + { + if (bytes[index + 1] == 0x31 && bytes[index] != 0x66) + { + headerOffset = bytes[index]; + bytes[index] = 0x66; + } + } + + public class CRC + { + private static readonly uint[] Table; + + static CRC() + { + Table = new uint[256]; + const uint kPoly = 0x9823D6E; + for (uint i = 0; i < 256; i++) + { + uint r = i; + for (int j = 0; j < 8; j++) + { + if ((r & 1) != 0) + r ^= kPoly; + r >>= 1; + } + Table[i] = r; + } + } + + uint _value = 0xFFFFFFFF; + + public void Update(byte[] data, uint offset, uint size) + { + for (uint i = 0; i < size; i++) + _value = (Table[(byte)_value ^ data[offset + i]] ^ (_value >> 8)) + 0x10; + } + + public uint GetDigest() { return ~_value - 0x7D29C488; } + + public static uint CalculateDigest(byte[] data, uint offset, uint size) + { + var crc = new CRC(); + crc.Update(data, offset, size); + return crc.GetDigest(); + } + } + + public static void RC4(Span data, int key) => RC4(data, BitConverter.GetBytes(key)); + + public static void RC4(Span data, byte[] key) + { + int[] S = new int[0x100]; + for (int _ = 0; _ < 0x100; _++) + { + S[_] = _; + } + + int[] T = new int[0x100]; + + if (key.Length == 0x100) + { + Buffer.BlockCopy(key, 0, T, 0, key.Length); + } + else + { + for (int _ = 0; _ < 0x100; _++) + { + T[_] = key[_ % key.Length]; + } + } + + int i = 0; + int j = 0; + for (i = 0; i < 0x100; i++) + { + j = (j + S[i] + T[i]) % 0x100; + + (S[j], S[i]) = (S[i], S[j]); + } + + i = j = 0; + for (int iteration = 0; iteration < data.Length; iteration++) + { + i = (i + 1) % 0x100; + j = (j + S[i]) % 0x100; + + (S[j], S[i]) = (S[i], S[j]); + var K = (uint)S[(S[j] + S[i]) % 0x100]; + + var k = (byte)(K << 6) | (K >> 2); + data[iteration] ^= (byte)(k + 0x3A); + } + } + } +} diff --git a/AssetStudio/Extensions/ByteArrayExtensions.cs b/AssetStudio/Extensions/ByteArrayExtensions.cs index e035aae..3159ba6 100644 --- a/AssetStudio/Extensions/ByteArrayExtensions.cs +++ b/AssetStudio/Extensions/ByteArrayExtensions.cs @@ -1,4 +1,5 @@ -using System.Text; +using System; +using System.Text; namespace AssetStudio { @@ -17,8 +18,8 @@ namespace AssetStudio } return buffer; } - public static int Search(this byte[] src, string value, int offset = 0) => Search(src, Encoding.UTF8.GetBytes(value), offset); - public static int Search(this byte[] src, byte[] pattern, int offset) + public static int Search(this byte[] src, string value, int offset = 0) => Search(src.AsSpan(), Encoding.UTF8.GetBytes(value), offset); + public static int Search(this Span src, byte[] pattern, int offset) { int maxFirstCharSlot = src.Length - pattern.Length + 1; for (int i = offset; i < maxFirstCharSlot; i++) diff --git a/AssetStudio/GameManager.cs b/AssetStudio/GameManager.cs index 06b43fb..dfa1a7e 100644 --- a/AssetStudio/GameManager.cs +++ b/AssetStudio/GameManager.cs @@ -31,6 +31,7 @@ namespace AssetStudio Games.Add(index++, new Game(GameType.FantasyOfWind)); Games.Add(index++, new Game(GameType.ShiningNikki)); Games.Add(index++, new Game(GameType.HelixWaltz2)); + Games.Add(index++, new Game(GameType.NetEase)); } public static Game GetGame(GameType gameType) => GetGame((int)gameType); public static Game GetGame(int index) @@ -132,7 +133,8 @@ namespace AssetStudio AlchemyStars, FantasyOfWind, ShiningNikki, - HelixWaltz2 + HelixWaltz2, + NetEase } public static class GameTypes @@ -152,6 +154,7 @@ namespace AssetStudio public static bool IsTOT(this GameType type) => type == GameType.TOT; public static bool IsNaraka(this GameType type) => type == GameType.Naraka; public static bool IsOPFP(this GameType type) => type == GameType.OPFP; + public static bool IsNetEase(this GameType type) => type == GameType.NetEase; 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/AssetStudioCLI/Components/CommandLine.cs b/AssetStudioCLI/Components/CommandLine.cs index 43a1476..f82f3a1 100644 --- a/AssetStudioCLI/Components/CommandLine.cs +++ b/AssetStudioCLI/Components/CommandLine.cs @@ -139,7 +139,7 @@ namespace AssetStudioCLI GameName.FromAmong(GameManager.GetGameNames()); GroupAssetsType.SetDefaultValue(AssetGroupOption.ByType); - MapOp.SetDefaultValue(MapOpType.Load); + MapOp.SetDefaultValue(MapOpType.None); MapType.SetDefaultValue(ExportListType.XML); } diff --git a/AssetStudioGUI/App.config b/AssetStudioGUI/App.config index 6886bc2..bf3422a 100644 --- a/AssetStudioGUI/App.config +++ b/AssetStudioGUI/App.config @@ -103,6 +103,9 @@ True + + False + \ No newline at end of file diff --git a/AssetStudioGUI/AssetBrowser.Designer.cs b/AssetStudioGUI/AssetBrowser.Designer.cs index b4e4a66..124417c 100644 --- a/AssetStudioGUI/AssetBrowser.Designer.cs +++ b/AssetStudioGUI/AssetBrowser.Designer.cs @@ -34,9 +34,9 @@ namespace AssetStudioGUI tableLayoutPanel1 = new TableLayoutPanel(); tableLayoutPanel2 = new TableLayoutPanel(); loadAssetMap = new Button(); - searchTextBox = new TextBox(); - loadSelected = new Button(); clear = new Button(); + loadSelected = new Button(); + searchTextBox = new TextBox(); ((System.ComponentModel.ISupportInitialize)assetListView).BeginInit(); tableLayoutPanel1.SuspendLayout(); tableLayoutPanel2.SuspendLayout(); @@ -91,45 +91,46 @@ namespace AssetStudioGUI // // loadAssetMap // + loadAssetMap.Dock = DockStyle.Fill; loadAssetMap.Location = new System.Drawing.Point(3, 3); loadAssetMap.Name = "loadAssetMap"; - loadAssetMap.Size = new System.Drawing.Size(94, 23); + loadAssetMap.Size = new System.Drawing.Size(114, 23); loadAssetMap.TabIndex = 0; loadAssetMap.Text = "Load AssetMap"; loadAssetMap.UseVisualStyleBackColor = true; loadAssetMap.Click += loadAssetMap_Click; - loadAssetMap.Dock = DockStyle.Fill; // - // searchTextBox + // clear // - searchTextBox.Location = new System.Drawing.Point(103, 3); - searchTextBox.Name = "searchTextBox"; - searchTextBox.Size = new System.Drawing.Size(227, 23); - searchTextBox.TabIndex = 3; - searchTextBox.KeyPress += searchTextBox_KeyPress; - searchTextBox.Dock = DockStyle.Fill; + clear.Dock = DockStyle.Fill; + clear.Location = new System.Drawing.Point(123, 3); + clear.Name = "clear"; + clear.Size = new System.Drawing.Size(54, 23); + clear.TabIndex = 1; + clear.Text = "Clear"; + clear.UseVisualStyleBackColor = true; + clear.Click += clear_Click; // // loadSelected // - loadSelected.Location = new System.Drawing.Point(336, 3); + loadSelected.Dock = DockStyle.Fill; + loadSelected.Location = new System.Drawing.Point(183, 3); loadSelected.Name = "loadSelected"; loadSelected.Size = new System.Drawing.Size(94, 23); loadSelected.TabIndex = 2; loadSelected.Text = "Load Selected"; loadSelected.UseVisualStyleBackColor = true; loadSelected.Click += loadSelected_Click; - loadSelected.Dock = DockStyle.Fill; // - // clear + // searchTextBox // - clear.Location = new System.Drawing.Point(436, 3); - clear.Name = "clear"; - clear.Size = new System.Drawing.Size(73, 23); - clear.TabIndex = 1; - clear.Text = "Clear"; - clear.UseVisualStyleBackColor = true; - clear.Click += clear_Click; - clear.Dock = DockStyle.Fill; + searchTextBox.Dock = DockStyle.Fill; + searchTextBox.Location = new System.Drawing.Point(283, 3); + searchTextBox.Name = "searchTextBox"; + searchTextBox.PlaceholderText = "Column Name=Regex{space}...."; + searchTextBox.Size = new System.Drawing.Size(232, 23); + searchTextBox.TabIndex = 3; + searchTextBox.KeyPress += searchTextBox_KeyPress; // // AssetBrowser // @@ -148,6 +149,8 @@ namespace AssetStudioGUI ResumeLayout(false); } + + #endregion private System.Windows.Forms.DataGridView assetListView; private TableLayoutPanel tableLayoutPanel1; diff --git a/AssetStudioGUI/AssetBrowser.cs b/AssetStudioGUI/AssetBrowser.cs index 588add1..7561633 100644 --- a/AssetStudioGUI/AssetBrowser.cs +++ b/AssetStudioGUI/AssetBrowser.cs @@ -1,5 +1,5 @@ using System; -using System.IO; +using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -15,6 +15,7 @@ namespace AssetStudioGUI { InitializeComponent(); _parent = form; + FormClosing += AssetBrowser_FormClosing; } private async void loadAssetMap_Click(object sender, EventArgs e) @@ -34,31 +35,50 @@ namespace AssetStudioGUI } private void clear_Click(object sender, EventArgs e) { - ResourceMap.Clear(); - assetListView.DataSource = null; + Clear(); Logger.Info($"Cleared !!"); } private async void loadSelected_Click(object sender, EventArgs e) { var files = assetListView.SelectedRows.Cast().Select(x => x.DataBoundItem as AssetEntry).Select(x => x.Source).ToHashSet(); - - if (files.Count != 0 && !files.Any(x => string.IsNullOrEmpty(x))) + if (files.Count != 0 && !files.Any(string.IsNullOrEmpty)) { Logger.Info("Loading..."); _parent.Invoke(() => _parent.LoadPaths(files.ToArray())); } } - private void searchTextBox_KeyPress(object sender, KeyPressEventArgs e) { if (e.KeyChar == (char)Keys.Enter) { + var filters = new Dictionary(); + var names = typeof(AssetEntry).GetProperties().Select(x => x.Name).ToList(); + var value = searchTextBox.Text; + var options = value.Split(' '); + for (int i = 0; i < options.Length; i++) + { + var option = options[i]; + var arguments = option.Split('='); + if (arguments.Length != 2) + { + Logger.Error($"Invalid argument at index {i + 1}"); + continue; + } + var (name, regex) = (arguments[0], arguments[1]); + if (!names.Contains(name, StringComparer.OrdinalIgnoreCase)) + { + Logger.Error($"Unknonw argument {name}"); + continue; + } + filters[name] = new Regex(regex, RegexOptions.IgnoreCase); + } + var assets = ResourceMap.GetEntries(); - if (assets.Length != 0 && !string.IsNullOrEmpty(value)) + if (assets.Length != 0) { var regex = new Regex(value, RegexOptions.IgnoreCase); - assetListView.DataSource = Array.FindAll(assets, x => x.Matches(regex)); + assetListView.DataSource = Array.FindAll(assets, x => x.Matches(filters)); } else { @@ -66,5 +86,15 @@ namespace AssetStudioGUI } } } + private void AssetBrowser_FormClosing(object sender, FormClosingEventArgs e) + { + Clear(); + base.OnClosing(e); + } + public void Clear() + { + ResourceMap.Clear(); + assetListView.DataSource = null; + } } } diff --git a/AssetStudioGUI/AssetStudioGUIForm.Designer.cs b/AssetStudioGUI/AssetStudioGUIForm.Designer.cs index 0f9dde2..78aaa8c 100644 --- a/AssetStudioGUI/AssetStudioGUIForm.Designer.cs +++ b/AssetStudioGUI/AssetStudioGUIForm.Designer.cs @@ -45,6 +45,7 @@ namespace AssetStudioGUI optionsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); displayAll = new System.Windows.Forms.ToolStripMenuItem(); enablePreview = new System.Windows.Forms.ToolStripMenuItem(); + enableModelPreview = new System.Windows.Forms.ToolStripMenuItem(); displayInfo = new System.Windows.Forms.ToolStripMenuItem(); toolStripMenuItem14 = new System.Windows.Forms.ToolStripMenuItem(); specifyUnityVersion = new System.Windows.Forms.ToolStripTextBox(); @@ -60,6 +61,9 @@ namespace AssetStudioGUI toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator(); exportSelectedObjectsmergeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); exportSelectedObjectsmergeWithAnimationClipToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + toolStripSeparator9 = new System.Windows.Forms.ToolStripSeparator(); + exportSelectedNodessplitToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + exportSelectedNodessplitSelectedAnimationClipsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); exportToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); exportAllAssetsMenuItem = new System.Windows.Forms.ToolStripMenuItem(); exportSelectedAssetsMenuItem = new System.Windows.Forms.ToolStripMenuItem(); @@ -157,9 +161,6 @@ namespace AssetStudioGUI exportAnimatorwithselectedAnimationClipMenuItem = new System.Windows.Forms.ToolStripMenuItem(); goToSceneHierarchyToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); showOriginalFileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - toolStripSeparator9 = new System.Windows.Forms.ToolStripSeparator(); - exportSelectedNodessplitToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - exportSelectedNodessplitSelectedAnimationClipsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); menuStrip1.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)splitContainer1).BeginInit(); splitContainer1.Panel1.SuspendLayout(); @@ -251,7 +252,7 @@ namespace AssetStudioGUI // // optionsToolStripMenuItem // - optionsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { displayAll, enablePreview, displayInfo, toolStripMenuItem14, toolStripMenuItem18, toolStripMenuItem19, showExpOpt }); + optionsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { displayAll, enablePreview, enableModelPreview, displayInfo, toolStripMenuItem14, toolStripMenuItem18, toolStripMenuItem19, showExpOpt }); optionsToolStripMenuItem.Name = "optionsToolStripMenuItem"; optionsToolStripMenuItem.Size = new System.Drawing.Size(61, 20); optionsToolStripMenuItem.Text = "Options"; @@ -276,6 +277,14 @@ namespace AssetStudioGUI enablePreview.ToolTipText = "Toggle the loading and preview of readable assets, such as images, sounds, text, etc.\r\nDisable preview if you have performance or compatibility issues."; enablePreview.CheckedChanged += enablePreview_Check; // + // enableModelPreview + // + enableModelPreview.CheckOnClick = true; + enableModelPreview.Name = "enableModelPreview"; + enableModelPreview.Size = new System.Drawing.Size(207, 22); + enableModelPreview.Text = "Enable model preview"; + enableModelPreview.CheckedChanged += enableModelPreview_CheckedChanged; + // // displayInfo // displayInfo.Checked = true; @@ -382,6 +391,25 @@ namespace AssetStudioGUI exportSelectedObjectsmergeWithAnimationClipToolStripMenuItem.Text = "Export selected objects (merge) + selected AnimationClips"; exportSelectedObjectsmergeWithAnimationClipToolStripMenuItem.Click += exportSelectedObjectsmergeWithAnimationClipToolStripMenuItem_Click; // + // toolStripSeparator9 + // + toolStripSeparator9.Name = "toolStripSeparator9"; + toolStripSeparator9.Size = new System.Drawing.Size(379, 6); + // + // exportSelectedNodessplitToolStripMenuItem + // + exportSelectedNodessplitToolStripMenuItem.Name = "exportSelectedNodessplitToolStripMenuItem"; + exportSelectedNodessplitToolStripMenuItem.Size = new System.Drawing.Size(382, 22); + exportSelectedNodessplitToolStripMenuItem.Text = "Export selected nodes (split)"; + exportSelectedNodessplitToolStripMenuItem.Click += exportSelectedNodessplitToolStripMenuItem_Click; + // + // exportSelectedNodessplitSelectedAnimationClipsToolStripMenuItem + // + exportSelectedNodessplitSelectedAnimationClipsToolStripMenuItem.Name = "exportSelectedNodessplitSelectedAnimationClipsToolStripMenuItem"; + exportSelectedNodessplitSelectedAnimationClipsToolStripMenuItem.Size = new System.Drawing.Size(382, 22); + exportSelectedNodessplitSelectedAnimationClipsToolStripMenuItem.Text = "Export selected nodes (split) + selected AnimationClips"; + exportSelectedNodessplitSelectedAnimationClipsToolStripMenuItem.Click += exportSelectedNodessplitSelectedAnimationClipsToolStripMenuItem_Click; + // // exportToolStripMenuItem // exportToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { exportAllAssetsMenuItem, exportSelectedAssetsMenuItem, exportFilteredAssetsMenuItem, toolStripSeparator3, exportAnimatorWithSelectedAnimationClipToolStripMenuItem, toolStripSeparator4, toolStripMenuItem2, toolStripMenuItem3, toolStripMenuItem16, toolStripSeparator2, toolStripMenuItem10 }); @@ -1229,25 +1257,6 @@ namespace AssetStudioGUI showOriginalFileToolStripMenuItem.Visible = false; showOriginalFileToolStripMenuItem.Click += showOriginalFileToolStripMenuItem_Click; // - // toolStripSeparator9 - // - toolStripSeparator9.Name = "toolStripSeparator9"; - toolStripSeparator9.Size = new System.Drawing.Size(379, 6); - // - // exportSelectedNodessplitToolStripMenuItem - // - exportSelectedNodessplitToolStripMenuItem.Name = "exportSelectedNodessplitToolStripMenuItem"; - exportSelectedNodessplitToolStripMenuItem.Size = new System.Drawing.Size(382, 22); - exportSelectedNodessplitToolStripMenuItem.Text = "Export selected nodes (split)"; - exportSelectedNodessplitToolStripMenuItem.Click += exportSelectedNodessplitToolStripMenuItem_Click; - // - // exportSelectedNodessplitSelectedAnimationClipsToolStripMenuItem - // - exportSelectedNodessplitSelectedAnimationClipsToolStripMenuItem.Name = "exportSelectedNodessplitSelectedAnimationClipsToolStripMenuItem"; - exportSelectedNodessplitSelectedAnimationClipsToolStripMenuItem.Size = new System.Drawing.Size(382, 22); - exportSelectedNodessplitSelectedAnimationClipsToolStripMenuItem.Text = "Export selected nodes (split) + selected AnimationClips"; - exportSelectedNodessplitSelectedAnimationClipsToolStripMenuItem.Click += exportSelectedNodessplitSelectedAnimationClipsToolStripMenuItem_Click; - // // AssetStudioGUIForm // AllowDrop = true; @@ -1425,6 +1434,7 @@ namespace AssetStudioGUI private System.Windows.Forms.ToolStripSeparator toolStripSeparator9; private System.Windows.Forms.ToolStripMenuItem exportSelectedNodessplitToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem exportSelectedNodessplitSelectedAnimationClipsToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem enableModelPreview; } } diff --git a/AssetStudioGUI/AssetStudioGUIForm.cs b/AssetStudioGUI/AssetStudioGUIForm.cs index 14d004d..bed4472 100644 --- a/AssetStudioGUI/AssetStudioGUIForm.cs +++ b/AssetStudioGUI/AssetStudioGUIForm.cs @@ -29,6 +29,7 @@ namespace AssetStudioGUI partial class AssetStudioGUIForm : Form { private AssetItem lastSelectedItem; + private AssetBrowser assetBrowser; private DirectBitmap imageTexture; private string tempClipboard; @@ -107,6 +108,7 @@ namespace AssetStudioGUI displayAll.Checked = Properties.Settings.Default.displayAll; displayInfo.Checked = Properties.Settings.Default.displayInfo; enablePreview.Checked = Properties.Settings.Default.enablePreview; + enableModelPreview.Checked = Properties.Settings.Default.modelsOnly; assetsManager.ResolveDependencies = Properties.Settings.Default.enableResolveDependencies; MiHoYoBinData.Encrypted = Properties.Settings.Default.encrypted; MiHoYoBinData.Key = Properties.Settings.Default.key; @@ -758,7 +760,7 @@ namespace AssetStudioGUI { switch (assetItem.Asset) { - case GameObject m_GameObject: + case GameObject m_GameObject when Properties.Settings.Default.modelsOnly: PreviewGameObject(m_GameObject); break; case Texture2D m_Texture2D: @@ -789,7 +791,7 @@ namespace AssetStudioGUI case Sprite m_Sprite: PreviewSprite(assetItem, m_Sprite); break; - case Animator m_Animator: + case Animator m_Animator when Properties.Settings.Default.modelsOnly: //StatusStripUpdate("Can be exported to FBX file."); PreviewAnimator(m_Animator); break; @@ -1748,6 +1750,23 @@ namespace AssetStudioGUI { visibleAssets = exportableAssets; } + if (Properties.Settings.Default.modelsOnly) + { + var models = visibleAssets.FindAll(x => x.Type == ClassIDType.Animator || x.Type == ClassIDType.GameObject); + foreach(var model in models) + { + var hasModel = model.Asset switch + { + GameObject m_GameObject => m_GameObject.HasModel(), + Animator m_Animator => m_Animator.m_GameObject.TryGet(out var gameObject) && gameObject.HasModel(), + _ => throw new NotImplementedException() + }; + if (!hasModel) + { + visibleAssets.Remove(model); + } + } + } if (listSearch.Text != " Filter ") { visibleAssets = visibleAssets.FindAll( @@ -1947,6 +1966,16 @@ namespace AssetStudioGUI Properties.Settings.Default.selectedAssetMapType = assetMapTypeComboBox.SelectedIndex; Properties.Settings.Default.Save(); } + private void enableModelPreview_CheckedChanged(object sender, EventArgs e) + { + Properties.Settings.Default.modelsOnly = enableModelPreview.Checked; + Properties.Settings.Default.Save(); + + if (visibleAssets.Count > 0) + { + FilterAssetList(); + } + } private void specifyGame_SelectedIndexChanged(object sender, EventArgs e) { @@ -2134,6 +2163,7 @@ namespace AssetStudioGUI { ResetForm(); AssetsHelper.Clear(); + assetBrowser.Clear(); assetsManager.SpecifyUnityVersion = specifyUnityVersion.Text; assetsManager.Game = Studio.Game; } @@ -2242,7 +2272,7 @@ namespace AssetStudioGUI private void loadAssetMapToolStripMenuItem_Click(object sender, EventArgs e) { - var assetBrowser = new AssetBrowser(this); + assetBrowser = new AssetBrowser(this); assetBrowser.Show(); } diff --git a/AssetStudioGUI/AssetStudioGUIForm.resx b/AssetStudioGUI/AssetStudioGUIForm.resx index ee3dd05..3d8c540 100644 --- a/AssetStudioGUI/AssetStudioGUIForm.resx +++ b/AssetStudioGUI/AssetStudioGUIForm.resx @@ -60,9 +60,6 @@ 312, 17 - - 432, 17 - abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWYZ 1234567890.:,;'\"(!?)+-*/= @@ -81,6 +78,9 @@ The quick brown fox jumps over the lazy dog. 1234567890 The quick brown fox jumps over the lazy dog. 1234567890 + + 432, 17 + 553, 17 diff --git a/AssetStudioGUI/Properties/Settings.Designer.cs b/AssetStudioGUI/Properties/Settings.Designer.cs index b80acd4..7ea06e8 100644 --- a/AssetStudioGUI/Properties/Settings.Designer.cs +++ b/AssetStudioGUI/Properties/Settings.Designer.cs @@ -418,5 +418,17 @@ namespace AssetStudioGUI.Properties { this["minimalAssetMap"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool modelsOnly { + get { + return ((bool)(this["modelsOnly"])); + } + set { + this["modelsOnly"] = value; + } + } } } diff --git a/AssetStudioGUI/Properties/Settings.settings b/AssetStudioGUI/Properties/Settings.settings index 0ef09de..154a3b2 100644 --- a/AssetStudioGUI/Properties/Settings.settings +++ b/AssetStudioGUI/Properties/Settings.settings @@ -101,5 +101,8 @@ True + + False + \ No newline at end of file