Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions Documentation/command-dump.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ UnityDataTool dump <path> [options]
| `-f, --output-format <format>` | Output format | `text` |
| `-s, --skip-large-arrays` | Skip dumping large arrays | `false` |
| `-i, --objectid <id>` | Only dump object with this ID | All objects |
| `-t, --type <type>` | Filter by object type (ClassID number or type name) | All objects |
| `-d, --typetree-data <file>` | Load an external TypeTree data file before processing (Unity 6.5+) | — |

## Examples
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion Documentation/textdumper.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
43 changes: 37 additions & 6 deletions TextDumper/TextDumperTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ 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)
{
if (string.IsNullOrWhiteSpace(typeFilter))
typeFilter = null;

m_SkipLargeArrays = skipLargeArrays;

try
Expand All @@ -38,7 +41,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);
}
}
}
Expand All @@ -56,7 +59,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)
Expand Down Expand Up @@ -378,8 +381,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))
{
Expand All @@ -400,6 +406,26 @@ void OutputSerializedFile(string path, long objectId)
continue;

var root = m_SerializedFile.GetTypeTreeRoot(obj.Id);

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 = root.Type;
if (!string.Equals(typeName, typeFilter, StringComparison.OrdinalIgnoreCase))
continue;
}
}

var offset = obj.Offset;

m_Writer.Write($"ID: {obj.Id} (ClassID: {obj.TypeId}) ");
Expand All @@ -408,8 +434,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}\".");
}
}
}

Expand Down
41 changes: 41 additions & 0 deletions UnityDataTool.Tests/UnityDataToolAssetBundleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
12 changes: 7 additions & 5 deletions UnityDataTool/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public static async Task<int> Main(string[] args)
var sOpt = new Option<bool>(aliases: new[] { "--skip-large-arrays", "-s" }, description: "Do not dump large arrays of basic data types");
var oOpt = new Option<DirectoryInfo>(aliases: new[] { "--output-path", "-o" }, description: "Output folder", getDefaultValue: () => new DirectoryInfo(Environment.CurrentDirectory));
var objectIdOpt = new Option<long>(aliases: new[] { "--objectid", "-i" }, () => 0, "Only dump the object with this signed 64-bit id (default: 0, dump all objects)");
var typeOpt = new Option<string>(aliases: new[] { "--type", "-t" }, description: "Filter by object type (ClassID number or type name)");

var dOpt = new Option<FileInfo>(aliases: new[] { "--typetree-data", "-d" }, description: typeTreeDataDescription);

Expand All @@ -102,16 +103,17 @@ public static async Task<int> 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);
}
Expand Down Expand Up @@ -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);
}
}

Expand Down
Loading