From 5a14048f44244814d021e14cea19dc1b41de9ba3 Mon Sep 17 00:00:00 2001 From: Ijtihed Date: Sun, 5 Apr 2026 13:13:32 +0300 Subject: [PATCH 1/2] Add type filtering to dump command (Issue #52) Added -t / --type option to the dump command. Accepts both numeric ClassID (-t 114) and type name (-t MonoBehaviour), case-insensitive. Resolved via TypeIdRegistry for built-in types, falls back to TypeTree root node for script types. Combines with -i as AND logic. Added "Filtering by Type" walkthrough in command-dump.md focused on the MonoBehaviour use case. Updated parameter docs in textdumper.md. Tests for name filter, numeric filter and no-match output. Made-with: Cursor --- Documentation/command-dump.md | 24 +++++++++++ Documentation/textdumper.md | 3 +- TextDumper/TextDumperTool.cs | 39 +++++++++++++++--- .../UnityDataToolAssetBundleTests.cs | 41 +++++++++++++++++++ UnityDataTool/Program.cs | 12 +++--- 5 files changed, 107 insertions(+), 12 deletions(-) diff --git a/Documentation/command-dump.md b/Documentation/command-dump.md index beb0a26..6f1d0d3 100644 --- a/Documentation/command-dump.md +++ b/Documentation/command-dump.md @@ -15,6 +15,7 @@ UnityDataTool dump [options] | `-f, --output-format ` | Output format | `text` | | `-s, --skip-large-arrays` | Skip dumping large arrays | `false` | | `-i, --objectid ` | Only dump object with this ID | All objects | +| `-t, --type ` | Filter by object type (ClassID number or type name) | All objects | | `-d, --typetree-data ` | Load an external TypeTree data file before processing (Unity 6.5+) | — | ## Examples @@ -39,6 +40,29 @@ Skip large arrays for cleaner output: UnityDataTool dump /path/to/file -s ``` +Dump only MonoBehaviour objects by type name: +```bash +UnityDataTool dump /path/to/file -t MonoBehaviour +``` + +Same thing using the numeric ClassID: +```bash +UnityDataTool dump /path/to/file -t 114 +``` + +Dump the AssetBundle manifest object: +```bash +UnityDataTool dump mybundle -t AssetBundle +``` + +--- + +## Filtering by Type + +The `-t` / `--type` option filters output to objects of a specific Unity type. It accepts either a numeric ClassID (e.g. `114`) or a type name (e.g. `MonoBehaviour`). Type name matching is case-insensitive. + +This is particularly useful for inspecting MonoBehaviour data in built AssetBundles. MonoBehaviour and ScriptableObject field values are serialized as binary, and a typical bundle contains many other object types (meshes, textures, materials, etc.). Using `-t MonoBehaviour` dumps only the scripting objects, showing the serialized C# field names, types, and values. + --- ## Archive Support diff --git a/Documentation/textdumper.md b/Documentation/textdumper.md index ffb9563..3ef490c 100644 --- a/Documentation/textdumper.md +++ b/Documentation/textdumper.md @@ -5,11 +5,12 @@ file (AssetBundle or SerializedFile) into human-readable yaml-style text file. ## How to use -The library consists of a single class called [TextDumperTool](../TextDumper/TextDumperTool.cs). It has a method named Dump and takes four parameters: +The library consists of a single class called [TextDumperTool](../TextDumper/TextDumperTool.cs). It has a method named Dump and takes five parameters: * path (string): path of the data file. * outputPath (string): path where the output files will be created. * skipLargeArrays (bool): if true, the content of arrays larger than 1KB won't be dumped. * objectId (long, optional): if specified and not 0, only the object with this signed 64-bit id will be dumped. If 0 (default), all objects are dumped. +* typeFilter (string, optional): if specified, only objects matching this type are dumped. Accepts a numeric ClassID (e.g. 114) or a type name (e.g. MonoBehaviour, case-insensitive). ## How to interpret the output files diff --git a/TextDumper/TextDumperTool.cs b/TextDumper/TextDumperTool.cs index 8f5da2c..bfbb237 100644 --- a/TextDumper/TextDumperTool.cs +++ b/TextDumper/TextDumperTool.cs @@ -14,7 +14,7 @@ public class TextDumperTool SerializedFile m_SerializedFile; StreamWriter m_Writer; - public int Dump(string path, string outputPath, bool skipLargeArrays, long objectId = 0) + public int Dump(string path, string outputPath, bool skipLargeArrays, long objectId = 0, string typeFilter = null) { m_SkipLargeArrays = skipLargeArrays; @@ -38,7 +38,7 @@ public int Dump(string path, string outputPath, bool skipLargeArrays, long objec { using (m_Writer = new StreamWriter(Path.Combine(outputPath, Path.GetFileName(node.Path) + ".txt"), false)) { - OutputSerializedFile("/" + node.Path, objectId); + OutputSerializedFile("/" + node.Path, objectId, typeFilter); } } } @@ -56,7 +56,7 @@ public int Dump(string path, string outputPath, bool skipLargeArrays, long objec { using (m_Writer = new StreamWriter(Path.Combine(outputPath, Path.GetFileName(path) + ".txt"), false)) { - OutputSerializedFile(path, objectId); + OutputSerializedFile(path, objectId, typeFilter); } } catch (SerializedFileOpenException) @@ -378,8 +378,11 @@ bool DumpManagedReferenceData(TypeTreeNode refTypeNode, TypeTreeNode referencedT return true; } - void OutputSerializedFile(string path, long objectId) + void OutputSerializedFile(string path, long objectId, string typeFilter) { + int filterTypeId = 0; + bool filterByTypeId = typeFilter != null && int.TryParse(typeFilter, out filterTypeId); + using (m_Reader = new UnityFileReader(path, 64 * 1024 * 1024)) using (m_SerializedFile = UnityFileSystem.OpenSerializedFile(path)) { @@ -399,6 +402,25 @@ void OutputSerializedFile(string path, long objectId) if (objectId != 0 && obj.Id != objectId) continue; + if (typeFilter != null) + { + if (filterByTypeId) + { + if (obj.TypeId != filterTypeId) + continue; + } + else + { + var typeName = TypeIdRegistry.GetTypeName(obj.TypeId); + // GetTypeName returns the id as a string when the type is unknown; + // fall back to the TypeTree root node for script types. + if (typeName == obj.TypeId.ToString()) + typeName = m_SerializedFile.GetTypeTreeRoot(obj.Id).Type; + if (!string.Equals(typeName, typeFilter, StringComparison.OrdinalIgnoreCase)) + continue; + } + } + var root = m_SerializedFile.GetTypeTreeRoot(obj.Id); var offset = obj.Offset; @@ -408,8 +430,13 @@ void OutputSerializedFile(string path, long objectId) dumpedObject = true; } - if (objectId != 0 && !dumpedObject) - m_Writer.WriteLine($"Object with ID {objectId} not found."); + if ((objectId != 0 || typeFilter != null) && !dumpedObject) + { + if (objectId != 0) + m_Writer.WriteLine($"Object with ID {objectId} not found."); + else + m_Writer.WriteLine($"No objects found matching type \"{typeFilter}\"."); + } } } diff --git a/UnityDataTool.Tests/UnityDataToolAssetBundleTests.cs b/UnityDataTool.Tests/UnityDataToolAssetBundleTests.cs index b47ae73..4fe7f03 100644 --- a/UnityDataTool.Tests/UnityDataToolAssetBundleTests.cs +++ b/UnityDataTool.Tests/UnityDataToolAssetBundleTests.cs @@ -144,6 +144,47 @@ public async Task DumpText_SkipLargeArrays_TextFileCreatedCorrectly( Assert.AreEqual(expected, content); } + [Test] + public async Task DumpText_TypeFilterByName_OnlyMatchingObjectsDumped() + { + var path = Path.Combine(Context.UnityDataFolder, "assetbundle"); + var outputFile = Path.Combine(m_TestOutputFolder, "CAB-5d40f7cad7c871cf2ad2af19ac542994.txt"); + + Assert.AreEqual(0, await Program.Main(new string[] { "dump", path, "-t", "MonoBehaviour" })); + Assert.IsTrue(File.Exists(outputFile)); + + var content = File.ReadAllText(outputFile); + Assert.That(content, Does.Contain("(ClassID: 114)")); + Assert.That(content, Does.Not.Contain("(ClassID: 1)")); + } + + [Test] + public async Task DumpText_TypeFilterByClassID_OnlyMatchingObjectsDumped() + { + var path = Path.Combine(Context.UnityDataFolder, "assetbundle"); + var outputFile = Path.Combine(m_TestOutputFolder, "CAB-5d40f7cad7c871cf2ad2af19ac542994.txt"); + + Assert.AreEqual(0, await Program.Main(new string[] { "dump", path, "-t", "114" })); + Assert.IsTrue(File.Exists(outputFile)); + + var content = File.ReadAllText(outputFile); + Assert.That(content, Does.Contain("(ClassID: 114)")); + Assert.That(content, Does.Not.Contain("(ClassID: 1)")); + } + + [Test] + public async Task DumpText_TypeFilterNoMatch_ShowsNotFoundMessage() + { + var path = Path.Combine(Context.UnityDataFolder, "assetbundle"); + var outputFile = Path.Combine(m_TestOutputFolder, "CAB-5d40f7cad7c871cf2ad2af19ac542994.txt"); + + Assert.AreEqual(0, await Program.Main(new string[] { "dump", path, "-t", "NonExistentType" })); + Assert.IsTrue(File.Exists(outputFile)); + + var content = File.ReadAllText(outputFile); + Assert.That(content, Does.Contain("No objects found matching type")); + } + [Test] public async Task Analyze_DefaultArgs_DatabaseCorrect() { diff --git a/UnityDataTool/Program.cs b/UnityDataTool/Program.cs index a65fd24..e2fa444 100644 --- a/UnityDataTool/Program.cs +++ b/UnityDataTool/Program.cs @@ -92,6 +92,7 @@ public static async Task Main(string[] args) var sOpt = new Option(aliases: new[] { "--skip-large-arrays", "-s" }, description: "Do not dump large arrays of basic data types"); var oOpt = new Option(aliases: new[] { "--output-path", "-o" }, description: "Output folder", getDefaultValue: () => new DirectoryInfo(Environment.CurrentDirectory)); var objectIdOpt = new Option(aliases: new[] { "--objectid", "-i" }, () => 0, "Only dump the object with this signed 64-bit id (default: 0, dump all objects)"); + var typeOpt = new Option(aliases: new[] { "--type", "-t" }, description: "Filter by object type (ClassID number or type name)"); var dOpt = new Option(aliases: new[] { "--typetree-data", "-d" }, description: typeTreeDataDescription); @@ -102,16 +103,17 @@ public static async Task Main(string[] args) sOpt, oOpt, objectIdOpt, + typeOpt, dOpt, }; dumpCommand.SetHandler( - (FileInfo fi, DumpFormat f, bool s, DirectoryInfo o, long objectId, FileInfo d) => + (FileInfo fi, DumpFormat f, bool s, DirectoryInfo o, long objectId, string type, FileInfo d) => { var ttResult = LoadTypeTreeDataFile(d); if (ttResult != 0) return Task.FromResult(ttResult); - return Task.FromResult(HandleDump(fi, f, s, o, objectId)); + return Task.FromResult(HandleDump(fi, f, s, o, objectId, type)); }, - pathArg, fOpt, sOpt, oOpt, objectIdOpt, dOpt); + pathArg, fOpt, sOpt, oOpt, objectIdOpt, typeOpt, dOpt); rootCommand.AddCommand(dumpCommand); } @@ -274,14 +276,14 @@ static int HandleFindReferences(FileInfo databasePath, string outputFile, long? } } - static int HandleDump(FileInfo filename, DumpFormat format, bool skipLargeArrays, DirectoryInfo outputFolder, long objectId = 0) + static int HandleDump(FileInfo filename, DumpFormat format, bool skipLargeArrays, DirectoryInfo outputFolder, long objectId = 0, string typeFilter = null) { switch (format) { case DumpFormat.Text: { var textDumper = new TextDumperTool(); - return textDumper.Dump(filename.FullName, outputFolder.FullName, skipLargeArrays, objectId); + return textDumper.Dump(filename.FullName, outputFolder.FullName, skipLargeArrays, objectId, typeFilter); } } From ad8437abc89b1ddbea4307652fcfc90ba110dded Mon Sep 17 00:00:00 2001 From: Ijtihed Date: Wed, 8 Apr 2026 09:14:54 +0300 Subject: [PATCH 2/2] Fix typeFilter edge cases: normalize whitespace input and hoist GetTypeTreeRoot call Made-with: Cursor --- TextDumper/TextDumperTool.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/TextDumper/TextDumperTool.cs b/TextDumper/TextDumperTool.cs index bfbb237..4a8f7a2 100644 --- a/TextDumper/TextDumperTool.cs +++ b/TextDumper/TextDumperTool.cs @@ -16,6 +16,9 @@ public class TextDumperTool public int Dump(string path, string outputPath, bool skipLargeArrays, long objectId = 0, string typeFilter = null) { + if (string.IsNullOrWhiteSpace(typeFilter)) + typeFilter = null; + m_SkipLargeArrays = skipLargeArrays; try @@ -402,6 +405,8 @@ void OutputSerializedFile(string path, long objectId, string typeFilter) if (objectId != 0 && obj.Id != objectId) continue; + var root = m_SerializedFile.GetTypeTreeRoot(obj.Id); + if (typeFilter != null) { if (filterByTypeId) @@ -415,13 +420,12 @@ void OutputSerializedFile(string path, long objectId, string typeFilter) // GetTypeName returns the id as a string when the type is unknown; // fall back to the TypeTree root node for script types. if (typeName == obj.TypeId.ToString()) - typeName = m_SerializedFile.GetTypeTreeRoot(obj.Id).Type; + typeName = root.Type; if (!string.Equals(typeName, typeFilter, StringComparison.OrdinalIgnoreCase)) continue; } } - var root = m_SerializedFile.GetTypeTreeRoot(obj.Id); var offset = obj.Offset; m_Writer.Write($"ID: {obj.Id} (ClassID: {obj.TypeId}) ");