Skip to content
Merged
9 changes: 8 additions & 1 deletion Documentation/command-analyze.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ UnityDataTool analyze <path> [options]
| `-s, --skip-references` | Skip CRC and reference extraction (faster, smaller DB) | `false` |
| `-v, --verbose` | Show more information during analysis | `false` |
| `--no-recurse` | Do not recurse into sub-directories | `false` |
| `-d, --typetree-data <file>` | Load an external TypeTree data file before processing (Unity 6.5+) | — |

## Examples

Expand Down Expand Up @@ -90,7 +91,13 @@ System.ArgumentException: Invalid object id.

This error occurs when SerializedFiles are built without TypeTrees. The command will skip these files and continue.

**Solution:** Enable **ForceAlwaysWriteTypeTrees** in your Unity build settings. See [Unity Content Format](../../Documentation/unity-content-format.md) for details.
**Solutions:**
- Enable **ForceAlwaysWriteTypeTrees** in your Unity build settings. See [Unity Content Format](../../Documentation/unity-content-format.md) for details.
- If your bundles were built with external TypeTree data (Unity 6.5+), use the `--typetree-data` option to load the TypeTree data file before analysis:

```bash
UnityDataTool analyze /path/to/bundles --typetree-data /path/to/typetree.bin
```

### SQL Constraint Errors

Expand Down
9 changes: 9 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 |
| `-d, --typetree-data <file>` | Load an external TypeTree data file before processing (Unity 6.5+) | — |

## Examples

Expand Down Expand Up @@ -87,6 +88,14 @@ UnityDataTool serialized-file metadata /path/to/file

The `TypeTree Definitions` field will show `No` when TypeTrees are absent.

**External TypeTree data (Unity 6.5+):**

If your bundles were built with TypeTree data extracted to a separate file, use the `--typetree-data` option to load it:

```bash
UnityDataTool dump /path/to/file.bundle --typetree-data /path/to/typetree.bin
```

---

## Output Format
Expand Down
16 changes: 16 additions & 0 deletions Documentation/unitydatatool.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@ Use `--help` with any command for details: `UnityDataTool analyze --help`

Use `--version` to print the tool version.

## External TypeTree Data

Starting with Unity 6.5, asset bundles can be built with TypeTree data extracted into a separate file. When bundles are built this way, the TypeTree data file must be loaded before the bundles can be processed.

The `--typetree-data` (`-d`) option is available on the [`analyze`](command-analyze.md) and [`dump`](command-dump.md) commands:

```bash
# Analyze bundles that use an external TypeTree data file
UnityDataTool analyze /path/to/bundles --typetree-data /path/to/typetree.bin

# Dump a bundle with external TypeTree data
UnityDataTool dump /path/to/file.bundle -d /path/to/typetree.bin
```

> **Note:** This option requires a version of UnityFileSystemApi from Unity 6.5 or newer. Using it with an older version of the library will produce an error message.


## Installation

Expand Down
115 changes: 115 additions & 0 deletions UnityDataTool.Tests/ExtractedTypeTreeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Data.Sqlite;
using NUnit.Framework;

namespace UnityDataTools.UnityDataTool.Tests;

#pragma warning disable NUnit2005, NUnit2006

public class ExtractedTypeTreeTests
{
private string m_TestOutputFolder;
private string m_DataFolder;
private string m_SerializedFile;
private string m_TypeTreeDataFile;

[OneTimeSetUp]
public void OneTimeSetup()
{
m_TestOutputFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "test_folder_typetree");
Directory.CreateDirectory(m_TestOutputFolder);

m_DataFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "ExtractedTypeTree");
m_SerializedFile = Path.Combine(m_DataFolder, "sfwithextractedtypetrees1");
m_TypeTreeDataFile = Path.Combine(m_DataFolder, "sfwithextractedtypetrees1.typetreedata");
}

[SetUp]
public void Setup()
{
Directory.SetCurrentDirectory(m_TestOutputFolder);
}

[TearDown]
public void Teardown()
{
SqliteConnection.ClearAllPools();

foreach (var file in new DirectoryInfo(m_TestOutputFolder).EnumerateFiles())
{
file.Delete();
}

foreach (var dir in new DirectoryInfo(m_TestOutputFolder).EnumerateDirectories())
{
dir.Delete(true);
}
}

[Test]
public async Task Analyze_WithTypeTreeData_DatabaseCorrect(
[Values("-d", "--typetree-data")] string option)
{
var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder);

Assert.AreEqual(0, await Program.Main(new string[] { "analyze", m_DataFolder, option, m_TypeTreeDataFile }));

using var db = SQLTestHelper.OpenDatabase(databasePath);

var objectCount = SQLTestHelper.QueryInt(db, "SELECT COUNT(*) FROM objects");
Assert.Greater(objectCount, 0, "Expected objects in database when TypeTree data file is provided");
}

[Test]
public async Task Analyze_WithoutTypeTreeData_ReportsFailure()
{
var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder);

using var swOut = new StringWriter();
using var swErr = new StringWriter();
var currentOut = Console.Out;
var currentErr = Console.Error;
try
{
Console.SetOut(swOut);
Console.SetError(swErr);

await Program.Main(new string[] { "analyze", m_DataFolder });

var output = swOut.ToString() + swErr.ToString();

Assert.That(output, Does.Contain("Failed files: 1"),
"Expected failure when analyzing without TypeTree data file");
}
finally
{
Console.SetOut(currentOut);
Console.SetError(currentErr);
}
}

[Test]
public async Task Dump_WithTypeTreeData_Succeeds(
[Values("-d", "--typetree-data")] string option)
{
Assert.AreEqual(0, await Program.Main(new string[] { "dump", m_SerializedFile, option, m_TypeTreeDataFile }));

var outputFiles = Directory.GetFiles(m_TestOutputFolder, "*.txt");
Assert.IsNotEmpty(outputFiles, "Expected dump output files when TypeTree data file is provided");
Comment thread
SkowronskiAndrew marked this conversation as resolved.
Outdated
}

[Test]
public async Task Dump_WithoutTypeTreeData_Fails()
{
Assert.AreNotEqual(0, await Program.Main(new string[] { "dump", m_SerializedFile }));
}

[Test]
public async Task TypeTreeData_FileNotFound_ReturnsError()
{
var result = await Program.Main(new string[] { "analyze", m_DataFolder, "--typetree-data", "nonexistent_file.bin" });
Assert.AreNotEqual(0, result, "Expected non-zero return code when TypeTree data file does not exist");
}
}
46 changes: 41 additions & 5 deletions UnityDataTool/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public static async Task<int> Main(string[] args)

var rootCommand = new RootCommand();

var typeTreeDataDescription = "Path to an external TypeTree data file to load before processing bundles";

{
var pathArg = new Argument<DirectoryInfo>("path", "The path to the directory containing the files to analyze").ExistingOnly();
var oOpt = new Option<string>(aliases: new[] { "--output-file", "-o" }, description: "Filename of the output database", getDefaultValue: () => "database.db");
Expand All @@ -32,6 +34,8 @@ public static async Task<int> Main(string[] args)
var vOpt = new Option<bool>(aliases: new[] { "--verbose", "-v" }, description: "Verbose output");
var recurseOpt = new Option<bool>(aliases: new[] { "--no-recurse" }, description: "Do not analyze contents of subdirectories inside path");

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

var analyzeCommand = new Command("analyze", "Analyze AssetBundles or SerializedFiles.")
{
pathArg,
Expand All @@ -40,13 +44,19 @@ public static async Task<int> Main(string[] args)
rOpt,
pOpt,
vOpt,
recurseOpt
recurseOpt,
dOpt
};

analyzeCommand.AddAlias("analyse");
analyzeCommand.SetHandler(
(DirectoryInfo di, string o, bool s, bool r, string p, bool v, bool recurseOpt) => Task.FromResult(HandleAnalyze(di, o, s, r, p, v, recurseOpt)),
pathArg, oOpt, sOpt, rOpt, pOpt, vOpt, recurseOpt);
(DirectoryInfo di, string o, bool s, bool r, string p, bool v, bool noRecurse, FileInfo d) =>
{
var ttResult = LoadTypeTreeDataFile(d);
if (ttResult != 0) return Task.FromResult(ttResult);
return Task.FromResult(HandleAnalyze(di, o, s, r, p, v, noRecurse));
},
pathArg, oOpt, sOpt, rOpt, pOpt, vOpt, recurseOpt, dOpt);

rootCommand.AddCommand(analyzeCommand);
}
Expand Down Expand Up @@ -83,17 +93,25 @@ public static async Task<int> Main(string[] args)
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 dOpt = new Option<FileInfo>(aliases: new[] { "--typetree-data", "-d" }, description: typeTreeDataDescription);

var dumpCommand = new Command("dump", "Dump the contents of an AssetBundle or SerializedFile.")
{
pathArg,
fOpt,
sOpt,
oOpt,
objectIdOpt,
dOpt,
};
dumpCommand.SetHandler(
(FileInfo fi, DumpFormat f, bool s, DirectoryInfo o, long objectId) => Task.FromResult(HandleDump(fi, f, s, o, objectId)),
pathArg, fOpt, sOpt, oOpt, objectIdOpt);
(FileInfo fi, DumpFormat f, bool s, DirectoryInfo o, long objectId, FileInfo d) =>
{
var ttResult = LoadTypeTreeDataFile(d);
if (ttResult != 0) return Task.FromResult(ttResult);
return Task.FromResult(HandleDump(fi, f, s, o, objectId));
},
pathArg, fOpt, sOpt, oOpt, objectIdOpt, dOpt);

rootCommand.AddCommand(dumpCommand);
}
Expand Down Expand Up @@ -199,6 +217,24 @@ enum DumpFormat
Text,
}

static int LoadTypeTreeDataFile(FileInfo typeTreeDataFile)
{
if (typeTreeDataFile == null)
return 0;

try
{
UnityFileSystem.AddTypeTreeSourceFromFile(typeTreeDataFile.FullName);
}
catch (EntryPointNotFoundException)
{
Console.Error.WriteLine("Error: The loaded UnityFileSystemApi does not support external TypeTree data files. Please update to Unity 6.5 or newer.");
Comment thread
paulb-unity marked this conversation as resolved.
Outdated
return 1;
}

return 0;
}

static int HandleAnalyze(
DirectoryInfo path,
string outputFile,
Expand Down
5 changes: 5 additions & 0 deletions UnityFileSystem/DllWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,11 @@ public static extern ReturnCode ReadFile(UnityFileHandle handle, long size,
public static extern ReturnCode GetRefTypeTypeTree(SerializedFileHandle handle, [MarshalAs(UnmanagedType.LPStr)] string className,
[MarshalAs(UnmanagedType.LPStr)] string namespaceName, [MarshalAs(UnmanagedType.LPStr)] string assemblyName, out TypeTreeHandle typeTree);

[DllImport("UnityFileSystemApi",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "UFS_AddTypeTreeSourceFromFile")]
public static extern ReturnCode AddTypeTreeSourceFromFile([MarshalAs(UnmanagedType.LPStr)] string path, out long handle);

[DllImport("UnityFileSystemApi",
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "UFS_GetTypeTreeNodeInfo")]
Expand Down
7 changes: 7 additions & 0 deletions UnityFileSystem/UnityFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ public static UnityFile OpenFile(string path)
return new UnityFile() { m_Handle = handle };
}

public static long AddTypeTreeSourceFromFile(string path)
{
var r = DllWrapper.AddTypeTreeSourceFromFile(path, out var handle);
HandleErrors(r, path);
return handle;
}

public static SerializedFile OpenSerializedFile(string path)
{
var r = DllWrapper.OpenSerializedFile(path, out var handle);
Expand Down
Loading