diff --git a/AGENTS.md b/AGENTS.md index 08b8cd6..3e8ca79 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,6 +47,23 @@ dotnet test --filter "FullyQualifiedName~SerializedFile" Test projects: UnityFileSystem.Tests, Analyzer.Tests, UnityDataTool.Tests, TestCommon (helper library) +### Code Style + +#### Comments + +* Write comments that explain "why". A few high level comments explaining the purpose of classes or methods is very helpful. Comments explaining tricky code are also helpful. +* Avoid comments that are redundant with the code. Do not comment before each line of code explaining what it does unless there is something that is not obvious going on. +* Do not use formal C# XML format when commenting methods, unless it is in an important interface class like UnityFileSystem. + +#### Formatting + +To repair white space or style issues, run: + +``` +dotnet format whitespace . --folder +dotnet format style +``` + ### Running the Tool ```bash # Show all commands @@ -61,6 +78,10 @@ UnityDataTool dump /path/to/file.bundle -o /output/path # Extract archive contents UnityDataTool archive extract file.bundle -o contents/ +# Quick inspect SerializedFile metadata +UnityDataTool serialized-file objectlist level0 +UnityDataTool sf externalrefs sharedassets0.assets --format json + # Find reference chains to an object UnityDataTool find-refs database.db -n "ObjectName" -t "Texture2D" ``` @@ -122,12 +143,15 @@ UnityDataTool (CLI executable) **Entry Points**: - `UnityDataTool/Program.cs` - CLI using System.CommandLine -- `UnityDataTool/Commands/` - Command handlers (Analyze.cs, Dump.cs, Archive.cs, FindReferences.cs) +- `UnityDataTool/SerializedFileCommands.cs` - SerializedFile inspection handlers +- `UnityDataTool/Archive.cs` - Archive manipulation handlers +- `Documentation/` - Command documentation (command-analyze.md, command-dump.md, command-archive.md, command-serialized-file.md, command-find-refs.md) **Core Libraries**: - `UnityFileSystem/UnityFileSystem.cs` - Init(), MountArchive(), OpenSerializedFile() - `UnityFileSystem/DllWrapper.cs` - P/Invoke bindings to native library - `UnityFileSystem/SerializedFile.cs` - Represents binary data files +- `UnityFileSystem/TypeIdRegistry.cs` - Built-in TypeId to type name mappings - `UnityFileSystem/RandomAccessReader.cs` - TypeTree property navigation **Analyzer**: @@ -207,7 +231,7 @@ The SQLite output uses views extensively to join base `objects` table with type- - `asset_view` - Explicitly assigned assets only - `shader_keyword_ratios` - Keyword variant analysis -See `Analyzer/README.md` and `Documentation/addressables-build-reports.md` for complete database schema documentation. +See `Documentation/analyzer.md` and `Documentation/addressables-build-reports.md` for complete database schema documentation. ### Common Issues diff --git a/Analyzer/Properties/Resources.Designer.cs b/Analyzer/Properties/Resources.Designer.cs index 0271b92..6eab80f 100644 --- a/Analyzer/Properties/Resources.Designer.cs +++ b/Analyzer/Properties/Resources.Designer.cs @@ -628,7 +628,67 @@ internal static string AudioClip { return ResourceManager.GetString("AudioClip", resourceCulture); } } - + + /// + /// Looks up a localized string similar to CREATE TABLE IF NOT EXISTS build_reports( + /// id INTEGER, + /// build_type TEXT, + /// build_result TEXT, + /// platform_name TEXT, + /// subtarget INTEGER, + /// start_time TEXT, + /// end_time TEXT, + /// total_time_seconds INTEGER, + /// total_size INTEGER, + /// build_guid TEXT, + /// total_errors INTEGER, + /// total_warnings INTEGER, + /// options INTEGER, + /// asset_bundle_options INTEGER, + /// output_path TEXT, + /// crc INTEGER, + /// PRIMARY KEY (id) + ///); + /// + ///CREATE TABLE IF NOT EXISTS build_report_files( + /// build_report_id INTEGER NOT NULL, + /// file_index INTEGER NOT NULL, + /// path TEXT NOT NULL, + /// role TEXT NOT NULL, + /// size INTEGER NOT NULL, + /// PRIMARY KEY (build_report_id, file_index), + /// FOREIGN KEY (build_report_id) REFERENCES build_reports(id) + ///); + /// + ///CREATE TABLE IF NOT EXISTS build_report_archive_contents( + /// build_report_id INTEGER NOT NULL, + /// assetbundle TEXT NOT NULL, + /// assetbundle_content TEXT NOT NULL, + /// PRIMARY KEY (build_report_id, assetbundle_content), + /// FOREIGN KEY (build_report_id) REFERENCES build_reports(id) + ///); + /// + ///CREATE VIEW build_report_files_view AS + ///SELECT + /// o.serialized_file, + /// br.id AS build_report_id, + /// br.build_type, + /// br.platform_name, + /// brf.file_index, + /// brf.path, + /// brf.role, + /// brf.size + ///FROM build_report_files brf + ///INNER JOIN build_reports br ON brf.build_report_id = br.id + ///INNER JOIN object_view o ON br.id = o.id; + ///. + /// + internal static string BuildReport { + get { + return ResourceManager.GetString("BuildReport", resourceCulture); + } + } + /// /// Looks up a localized string similar to CREATE INDEX refs_object_index ON refs(object); ///CREATE INDEX refs_referenced_object_index ON refs(referenced_object); @@ -790,5 +850,113 @@ internal static string Texture2D { return ResourceManager.GetString("Texture2D", resourceCulture); } } + + /// + /// Looks up a localized string similar to CREATE TABLE IF NOT EXISTS monoscripts( + /// id INTEGER, + /// class_name TEXT, + /// namespace TEXT, + /// assembly_name TEXT, + /// PRIMARY KEY (id) + ///); + /// + ///CREATE VIEW monoscript_view AS + ///SELECT + /// o.id, + /// o.object_id, + /// o.asset_bundle, + /// o.serialized_file, + /// m.class_name, + /// m.namespace, + /// m.assembly_name + ///FROM object_view o INNER JOIN monoscripts m ON o.id = m.id; + /// + ///CREATE VIEW script_object_view AS + ///SELECT + /// mb.id, + /// mb.object_id, + /// mb.asset_bundle, + /// mb.serialized_file, + /// mb.name, + /// mb.type, + /// mb.size, + /// mb.pretty_size, + /// ms.class_name, + /// ms.namespace, + /// ms.assembly_name + ///FROM object_view mb + ///INNER JOIN refs r ON mb.id = r.object + ///INNER JOIN monoscript_view ms ON r.referenced_object = ms.id + ///WHERE mb.type = 'MonoBehaviour' AND r.property_type = 'MonoScript'; + ///. + /// + internal static string MonoScript { + get { + return ResourceManager.GetString("MonoScript", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CREATE TABLE IF NOT EXISTS build_report_packed_assets( + /// id INTEGER, + /// path TEXT, + /// file_header_size INTEGER, + /// PRIMARY KEY (id) + ///); + /// + ///CREATE TABLE IF NOT EXISTS build_report_source_assets( + /// id INTEGER PRIMARY KEY AUTOINCREMENT, + /// source_asset_guid TEXT NOT NULL, + /// build_time_asset_path TEXT NOT NULL, + /// UNIQUE(source_asset_guid, build_time_asset_path) + ///); + /// + ///CREATE TABLE IF NOT EXISTS build_report_packed_asset_info( + /// packed_assets_id INTEGER, + /// object_id INTEGER, + /// type INTEGER, + /// size INTEGER, + /// offset INTEGER, + /// source_asset_id INTEGER NOT NULL, + /// FOREIGN KEY (packed_assets_id) REFERENCES build_report_packed_assets(id), + /// FOREIGN KEY (source_asset_id) REFERENCES build_report_source_assets(id) + ///); + /// + ///CREATE VIEW build_report_packed_assets_view AS + ///SELECT + /// pa.id, + /// o.object_id, + /// brac.assetbundle, + /// sf.name as build_report, + /// pa.path, + /// pa.file_header_size + ///FROM build_report_packed_assets pa + ///INNER JOIN objects o ON pa.id = o.id + ///INNER JOIN serialized_files sf ON o.serialized_file = sf.id + ///LEFT JOIN objects br_obj ON o.serialized_file = br_obj.serialized_file AND br_obj.type = 1125 + ///LEFT JOIN build_report_archive_contents brac ON br_obj.id = brac.build_report_id AND pa.path = brac.assetbundle_content; + /// + ///CREATE VIEW build_report_packed_asset_contents_view AS + ///SELECT + /// o.serialized_file, + /// pa.path, + /// pac.packed_assets_id, + /// pac.object_id, + /// pac.type, + /// pac.size, + /// pac.offset, + /// sa.source_asset_guid, + /// sa.build_time_asset_path + ///FROM build_report_packed_asset_info pac + ///LEFT JOIN build_report_packed_assets pa ON pac.packed_assets_id = pa.id + ///LEFT JOIN object_view o ON o.id = pa.id + ///LEFT JOIN build_report_source_assets sa ON pac.source_asset_id = sa.id; + ///. + /// + internal static string PackedAssets { + get { + return ResourceManager.GetString("PackedAssets", resourceCulture); + } + } } } diff --git a/Analyzer/Properties/Resources.resx b/Analyzer/Properties/Resources.resx index bc44646..1722f57 100644 --- a/Analyzer/Properties/Resources.resx +++ b/Analyzer/Properties/Resources.resx @@ -127,6 +127,9 @@ ..\Resources\AudioClip.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + ..\Resources\BuildReport.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + ..\Resources\Finalize.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 @@ -223,4 +226,10 @@ ..\Resources\AddrBuildSchemaDataPairs.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + ..\Resources\MonoScript.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Resources\PackedAssets.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + diff --git a/Analyzer/README.md b/Analyzer/README.md index 574711b..5d4c2b9 100644 --- a/Analyzer/README.md +++ b/Analyzer/README.md @@ -1,204 +1,3 @@ # Analyzer -The Analyzer is a class library that can be used to analyze the content of Unity data files such -as AssetBundles and SerializedFiles. It iterates through all the serialized objects and uses the -TypeTree to extract information about these objects (e.g. name, size, etc.) - -The most common use of this library is through the [analyze](../UnityDataTool/README.md#analyzeanalyse) -command of the UnityDataTool. This uses the Analyze library to generate a SQLite database. - -Once generated, a tool such as the [DB Browser for SQLite](https://sqlitebrowser.org/), or the command line `sqlite3` tool, can be used to look at the content of the database. - -# Example usage - -See [this topic](../Documentation/analyze-examples.md) for examples of how to use the SQLite output of the UnityDataTool Analyze command. - -# DataBase Reference - -The database provides different views. The views join multiple tables together and often it is not necessary to write your own SQL queries to find the information you want, especially when you are using a visual SQLite tool. - -This section gives an overview of the main views. - -## object_view - -This is the main view where the information about all the objects in the AssetBundles is available. -Its columns are: -* id: a unique id without any meaning outside of the database -* object_id: the Unity object id (unique inside its SerializedFile but not necessarily acros all - AssetBundles) -* asset_bundle: the name of the AssetBundle containing the object (will be null if the source file - was a SerializedFile and not an AssetBundle) -* serialized_file: the name of the SerializedFile containing the object -* type: the type of the object -* name: the name of the object, if it had one -* game_object: the id of the GameObject containing this object, if any (mostly for Components) -* size: the size of the object in bytes (e.g. 3343772) -* pretty_size: the size in an easier to read format (e.g. 3.2 MB) - -## view_breakdown_by_type - -This view lists the total number and size of the objects, aggregated by type. - -## view_potential_duplicates - -This view lists the objects that are possibly included more than once in the AssetBundles. This can -happen when an asset is referenced from multiple AssetBundles but is not assigned to one. In this -case, Unity will include the asset in all the AssetBundles with a reference to it. The -view_potential_duplicates provides the number of instances and the total size of the potentially -duplicated assets. It also lists all the AssetBundles where the asset was found. - -If the skipReferences option is used, there will be a lot of false positives in that view. Otherwise, -it should be very accurate because CRCs are used to determine if objects are identical. - -## asset_view (AssetBundleProcessor) - -This view lists all the assets that have been explicitly assigned to AssetBundles. The dependencies -that were automatically added by Unity at build time won't appear in this view. The columns are the -same as those in the *object_view* with the addition of the *asset_name* that contains the filename -of the asset. - -## asset_dependencies_view (AssetBundleProcessor) - -This view lists the dependencies of all the assets. You can filter by id or asset_name to get all -the dependencies of an asset. Conversely, filtering by dep_id will return all the assets that -depend on this object. This can be useful to figure out why an asset was included in a build. - -## animation_view (AnimationClipProcessor) - -This provides additional information about AnimationClips. The columns are the same as those in -the *object_view*, with the addition of: -* legacy: 1 if it's a legacy animation, 0 otherwise -* events: the number of events - -## audio_clip_view (AudioClipProcessor) - -This provides additional information about AudioClips. The columns are the same as those in -the *object_view*, with the addition of: -* bits_per_sample: number of bits per sample -* frequency: sampling frequency -* channels: number of channels -* load_type: either *Compressed in Memory*, *Decompress on Load* or *Streaming* -* format: compression format - -## mesh_view (MeshProcessor) - -This provides additional information about Meshes. The columns are the same as those in -the *object_view*, with the addition of: -* sub_meshes: the number of sub-meshes -* blend_shapes: the number of blend shapes -* bones: the number of bones -* indices: the number of vertex indices -* vertices: the number of vertices -* compression: 1 if compressed, 0 otherwise -* rw_enabled: 1 if the mesh has the *R/W Enabled* option, 0 otherwise -* vertex_size: number of bytes used by each vertex -* channels: name and type of the vertex channels - -## texture_view (Texture2DProcessor) - -This provides additional information about Texture2Ds. The columns are the same as those in -the *object_view*, with the addition of: -* width/height: texture resolution -* format: compression format -* mip_count: number of mipmaps -* rw_enabled: 1 if the mesh has the *R/W Enabled* option, 0 otherwise - -## shader_view (ShaderProcessor) - -This provides additional information about Shaders. The columns are the same as those in -the *object_view*, with the addition of: -* decompressed_size: the approximate size in bytes that this shader will need at runtime when - loaded -* sub_shaders: the number of sub-shaders -* sub_programs: the number of sub-programs (usually one per shader variant, stage and pass) -* unique_programs: the number of unique program (variants with identical programs will share the - same program in memory) -* keywords: list of all the keywords affecting the shader - -## shader_subprogram_view (ShaderProcessor) - -This view lists all the shader sub-programs and has the same columns as the *shader_view* with the -addition of: -* api: the API of the shader (e.g. DX11, Metal, GLES, etc.) -* pass: the pass number of the sub-program -* pass_name: the pass name, if available -* hw_tier: the hardware tier of the sub-program (as defined in the Graphics settings) -* shader_type: the type of shader (e.g. vertex, fragment, etc.) -* sub_program: the subprogram index for this pass and shader type -* keywords: the shader keywords specific to this sub-program - -## shader_keyword_ratios - -This view can help to determine which shader keywords are causing a large number of variants. To -understand how it works, let's define a "program" as a unique combination of shader, subshader, -hardware tier, pass number, API (DX, Metal, etc.), and shader type (vertex, fragment, etc). - -Each row of the view corresponds to a combination of one program and one of its keywords. The -columns are: - -* shader_id: the shader id -* name: the shader name -* sub_shader: the sub-shader number -* hw_tier: the hardware tier of the sub-program (as defined in the Graphics settings) -* pass: the pass number of the sub-program -* api: the API of the shader (e.g. DX11, Metal, GLES, etc.) -* pass_name: the pass name, if available -* shader_type: the type of shader (e.g. vertex, fragment, etc.) -* total_variants: total number of variants for this program. -* keyword: one of the program's keywords -* variants: number of variants including this keyword. -* ratio: variants/total_variants - -The ratio can be used to determine how a keyword affects the number of variants. When it is equal -to 0.5, it means that it is in half of the variants. Basically, that means that it is not stripped -at all because each of the program's variants has a version with and without that keyword. -Therefore, keywords with a ratio close to 0.5 are good targets for stripping. When the ratio is -close to 0 or 1, it means that the keyword is in almost none or almost all of the variants and -stripping it won't make a big difference. - -## view_breakdowns_shaders (ShaderProcessor) - -This view lists all the shaders aggregated by name. The *instances* column indicates how many time -the shader was found in the data files. It also provides the total size per shader and the list of -AssetBundles in which they were found. - -# Advanced - -## Using the library - -The [AnalyzerTool](./AnalyzerTool.cs) class is the API entry point. The main method is called -Analyze. It is currently hard coded to write using the [SQLiteWriter](./SQLite/SQLiteWriter.cs), -but this approach could be extended to add support for other outputs. - -Calling this method will recursively process the files matching the search pattern in the provided -path. It will add a row in the 'objects' table for each serialized object. This table contain basic -information such as the size and the name of the object (if it has one). - -## Extending the Library - -The extracted information is forwarded to an object implementing the [IWriter](./IWriter.cs) -interface. The library provides the [SQLiteWriter](./SQLite/SQLiteWriter.cs) implementation that -writes the data into a SQLite database. - -The core properties that apply to all Unity Objects are extracted into the `objects` table. -However much of the most useful Analyze functionality comes by virtue of the type-specific information that is extracted for -important types like Meshes, Shaders, Texture2D and AnimationClips. For example, when a Mesh object is encountered in a Serialized -File, then rows are added to both the `objects` table and the `meshes` table. The meshes table contains columns that only apply to Mesh objects, for example the number of vertices, indices, bones, and channels. The `mesh_view` is a view that joins the `objects` table with the `meshes` table, so that you can see all the properties of a Mesh object in one place. - -Each supported Unity object type follows the same pattern: -* A Handler class in the SQLite/Handlers, e.g. [MeshHandler.cs](./SQLite/Handler/MeshHandler.cs). -* The registration of the handler in the m_Handlers dictionary in [SerializedFileSQLiteWriter.cs](./SQLite/Writers/SerializedFileSQLiteWriter.cs). -* SQL statements defining extra tables and views associated with the type, e.g. [Mesh.sql](./SQLite/Resources/Mesh.sql). -* A Reader class that uses RandomAccessReader to read properties from the serialized object, e.g. [Mesh.cs](./SerializedObjects/Mesh.cs). - -It would be possible to extend the Analyze library to add additional columns for the existing types, or by following the same pattern to add additional types. The [dump](../UnityDataTool/README.md#dump) feature of UnityDataTool is a useful way to see the property names and other details of the serialization for a type. Based on that information, code in the Reader class can use the RandomAccessReader to retrieve those properties to bring them into the SQLite database. - -## Supporting Other File Formats - -Another direction of possible extension is to support analyzing additional file formats, beyond Unity SerializedFiles. - -This the approach taken to analyze Addressables Build Layout files, which are JSON files using the format defined in [BuildLayout.cs](./SQLite/Parsers/Models/BuildLayout.cs). - -Support for another file format could be added by deriving an additional class from SQLiteWriter and implementing a class derived from ISQLiteFileParser. Then follow the existing code structure convention to add new Commands (derived from AbstractCommand) and Resource .sql files to establish additional tables in the database. - -An example of another file format that could be useful to support, as the tool evolves, are the yaml [.manifest files](https://docs.unity3d.com/Manual/assetbundles-file-format.html), generated by BuildPipeline.BuildAssetBundles(). +See [Documentation/analyzer.md](../Documentation/analyzer.md) diff --git a/Analyzer/Resources/BuildReport.sql b/Analyzer/Resources/BuildReport.sql new file mode 100644 index 0000000..eab8bb9 --- /dev/null +++ b/Analyzer/Resources/BuildReport.sql @@ -0,0 +1,52 @@ +CREATE TABLE IF NOT EXISTS build_reports( + id INTEGER, + build_type TEXT, + build_result TEXT, + platform_name TEXT, + subtarget INTEGER, + start_time TEXT, + end_time TEXT, + total_time_seconds INTEGER, + total_size INTEGER, + build_guid TEXT, + total_errors INTEGER, + total_warnings INTEGER, + options INTEGER, + asset_bundle_options INTEGER, + output_path TEXT, + crc INTEGER, + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS build_report_files( + build_report_id INTEGER NOT NULL, + file_index INTEGER NOT NULL, + path TEXT NOT NULL, + role TEXT NOT NULL, + size INTEGER NOT NULL, + PRIMARY KEY (build_report_id, file_index), + FOREIGN KEY (build_report_id) REFERENCES build_reports(id) +); + +CREATE TABLE IF NOT EXISTS build_report_archive_contents( + build_report_id INTEGER NOT NULL, + assetbundle TEXT NOT NULL, + assetbundle_content TEXT NOT NULL, + PRIMARY KEY (build_report_id, assetbundle_content), + FOREIGN KEY (build_report_id) REFERENCES build_reports(id) +); + +CREATE VIEW build_report_files_view AS +SELECT + o.serialized_file, + br.id AS build_report_id, + br.build_type, + br.platform_name, + brf.file_index, + brf.path, + brf.role, + brf.size +FROM build_report_files brf +INNER JOIN build_reports br ON brf.build_report_id = br.id +INNER JOIN object_view o ON br.id = o.id; + diff --git a/Analyzer/Resources/MonoScript.sql b/Analyzer/Resources/MonoScript.sql new file mode 100644 index 0000000..3b0a36b --- /dev/null +++ b/Analyzer/Resources/MonoScript.sql @@ -0,0 +1,34 @@ +CREATE TABLE IF NOT EXISTS monoscripts( + id INTEGER, + class_name TEXT, + namespace TEXT, + assembly_name TEXT, + PRIMARY KEY (id) +); + +CREATE VIEW monoscript_view AS +SELECT + o.id, + o.object_id, + o.asset_bundle, + o.serialized_file, + m.class_name, + m.namespace, + m.assembly_name +FROM object_view o INNER JOIN monoscripts m ON o.id = m.id; + +CREATE VIEW script_object_view AS +SELECT + mb.id, + mb.object_id, + mb.asset_bundle, + mb.serialized_file, + ms.class_name, + ms.namespace, + ms.assembly_name, + mb.name, + mb.size +FROM object_view mb +INNER JOIN refs r ON mb.id = r.object +INNER JOIN monoscript_view ms ON r.referenced_object = ms.id +WHERE mb.type = 'MonoBehaviour' AND r.property_type = 'MonoScript'; diff --git a/Analyzer/Resources/PackedAssets.sql b/Analyzer/Resources/PackedAssets.sql new file mode 100644 index 0000000..dd55bb0 --- /dev/null +++ b/Analyzer/Resources/PackedAssets.sql @@ -0,0 +1,61 @@ +CREATE TABLE IF NOT EXISTS build_report_packed_assets( + id INTEGER, + path TEXT, + file_header_size INTEGER, + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS build_report_source_assets( + id INTEGER PRIMARY KEY AUTOINCREMENT, + source_asset_guid TEXT NOT NULL, + build_time_asset_path TEXT NOT NULL, + UNIQUE(source_asset_guid, build_time_asset_path) +); + +CREATE TABLE IF NOT EXISTS build_report_packed_asset_info( + packed_assets_id INTEGER, + object_id INTEGER, + type INTEGER, + size INTEGER, + offset INTEGER, + source_asset_id INTEGER NOT NULL, + FOREIGN KEY (packed_assets_id) REFERENCES build_report_packed_assets(id), + FOREIGN KEY (source_asset_id) REFERENCES build_report_source_assets(id) +); + +CREATE VIEW build_report_packed_assets_view AS +SELECT + pa.id, + o.object_id, + brac.assetbundle, + pa.path, + pa.file_header_size, + br_obj.id as build_report_id, + sf.name as build_report_filename +FROM build_report_packed_assets pa +INNER JOIN objects o ON pa.id = o.id +INNER JOIN serialized_files sf ON o.serialized_file = sf.id +LEFT JOIN objects br_obj ON o.serialized_file = br_obj.serialized_file AND br_obj.type = 1125 +LEFT JOIN build_report_archive_contents brac ON br_obj.id = brac.build_report_id AND pa.path = brac.assetbundle_content; + +CREATE VIEW build_report_packed_asset_contents_view AS +SELECT + sf.name as serialized_file, + brac.assetbundle, + pa.path, + pac.packed_assets_id, + pac.object_id, + pac.type, + pac.size, + pac.offset, + sa.source_asset_guid, + sa.build_time_asset_path, + br_obj.id as build_report_id +FROM build_report_packed_asset_info pac +LEFT JOIN build_report_packed_assets pa ON pac.packed_assets_id = pa.id +LEFT JOIN objects o ON o.id = pa.id +INNER JOIN serialized_files sf ON o.serialized_file = sf.id +LEFT JOIN build_report_source_assets sa ON pac.source_asset_id = sa.id +LEFT JOIN objects br_obj ON o.serialized_file = br_obj.serialized_file AND br_obj.type = 1125 +LEFT JOIN build_report_archive_contents brac ON br_obj.id = brac.build_report_id AND pa.path = brac.assetbundle_content; + diff --git a/Analyzer/SQLite/Handlers/BuildReportHandler.cs b/Analyzer/SQLite/Handlers/BuildReportHandler.cs new file mode 100644 index 0000000..8535ab1 --- /dev/null +++ b/Analyzer/SQLite/Handlers/BuildReportHandler.cs @@ -0,0 +1,132 @@ +using System; +using Microsoft.Data.Sqlite; +using UnityDataTools.Analyzer.SerializedObjects; +using UnityDataTools.FileSystem.TypeTreeReaders; + +namespace UnityDataTools.Analyzer.SQLite.Handlers; + +public class BuildReportHandler : ISQLiteHandler +{ + private SqliteCommand m_InsertCommand; + private SqliteCommand m_InsertFileCommand; + private SqliteCommand m_InsertArchiveContentCommand; + + public void Init(SqliteConnection db) + { + using var command = db.CreateCommand(); + command.CommandText = Properties.Resources.BuildReport ?? throw new InvalidOperationException("BuildReport resource not found"); + command.ExecuteNonQuery(); + + m_InsertCommand = db.CreateCommand(); + m_InsertCommand.CommandText = @"INSERT INTO build_reports( + id, build_type, build_result, platform_name, subtarget, start_time, end_time, total_time_seconds, + total_size, build_guid, total_errors, total_warnings, options, asset_bundle_options, + output_path, crc + ) VALUES( + @id, @build_type, @build_result, @platform_name, @subtarget, @start_time, @end_time, @total_time_seconds, + @total_size, @build_guid, @total_errors, @total_warnings, @options, @asset_bundle_options, + @output_path, @crc + )"; + + m_InsertCommand.Parameters.Add("@id", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@build_type", SqliteType.Text); + m_InsertCommand.Parameters.Add("@build_result", SqliteType.Text); + m_InsertCommand.Parameters.Add("@platform_name", SqliteType.Text); + m_InsertCommand.Parameters.Add("@subtarget", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@start_time", SqliteType.Text); + m_InsertCommand.Parameters.Add("@end_time", SqliteType.Text); + m_InsertCommand.Parameters.Add("@total_time_seconds", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@total_size", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@build_guid", SqliteType.Text); + m_InsertCommand.Parameters.Add("@total_errors", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@total_warnings", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@options", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@asset_bundle_options", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@output_path", SqliteType.Text); + m_InsertCommand.Parameters.Add("@crc", SqliteType.Integer); + + m_InsertFileCommand = db.CreateCommand(); + m_InsertFileCommand.CommandText = @"INSERT INTO build_report_files( + build_report_id, file_index, path, role, size + ) VALUES( + @build_report_id, @file_index, @path, @role, @size + )"; + + m_InsertFileCommand.Parameters.Add("@build_report_id", SqliteType.Integer); + m_InsertFileCommand.Parameters.Add("@file_index", SqliteType.Integer); + m_InsertFileCommand.Parameters.Add("@path", SqliteType.Text); + m_InsertFileCommand.Parameters.Add("@role", SqliteType.Text); + m_InsertFileCommand.Parameters.Add("@size", SqliteType.Integer); + + m_InsertArchiveContentCommand = db.CreateCommand(); + m_InsertArchiveContentCommand.CommandText = @"INSERT INTO build_report_archive_contents( + build_report_id, assetbundle, assetbundle_content + ) VALUES( + @build_report_id, @assetbundle, @assetbundle_content + )"; + + m_InsertArchiveContentCommand.Parameters.Add("@build_report_id", SqliteType.Integer); + m_InsertArchiveContentCommand.Parameters.Add("@assetbundle", SqliteType.Text); + m_InsertArchiveContentCommand.Parameters.Add("@assetbundle_content", SqliteType.Text); + } + + public void Process(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize) + { + var buildReport = BuildReport.Read(reader); + m_InsertCommand.Transaction = ctx.Transaction; + m_InsertCommand.Parameters["@id"].Value = objectId; + m_InsertCommand.Parameters["@build_type"].Value = BuildReport.GetBuildTypeString(buildReport.BuildType); + m_InsertCommand.Parameters["@build_result"].Value = buildReport.BuildResult; + m_InsertCommand.Parameters["@platform_name"].Value = buildReport.PlatformName; + m_InsertCommand.Parameters["@subtarget"].Value = buildReport.Subtarget; + m_InsertCommand.Parameters["@start_time"].Value = buildReport.StartTime; + m_InsertCommand.Parameters["@end_time"].Value = buildReport.EndTime; + m_InsertCommand.Parameters["@total_time_seconds"].Value = buildReport.TotalTimeSeconds; + m_InsertCommand.Parameters["@total_size"].Value = (long)buildReport.TotalSize; + m_InsertCommand.Parameters["@build_guid"].Value = buildReport.BuildGuid; + m_InsertCommand.Parameters["@total_errors"].Value = buildReport.TotalErrors; + m_InsertCommand.Parameters["@total_warnings"].Value = buildReport.TotalWarnings; + m_InsertCommand.Parameters["@options"].Value = buildReport.Options; + m_InsertCommand.Parameters["@asset_bundle_options"].Value = buildReport.AssetBundleOptions; + m_InsertCommand.Parameters["@output_path"].Value = buildReport.OutputPath; + m_InsertCommand.Parameters["@crc"].Value = buildReport.Crc; + + m_InsertCommand.ExecuteNonQuery(); + + // Insert files + foreach (var file in buildReport.Files) + { + m_InsertFileCommand.Transaction = ctx.Transaction; + m_InsertFileCommand.Parameters["@build_report_id"].Value = objectId; + m_InsertFileCommand.Parameters["@file_index"].Value = file.Id; + m_InsertFileCommand.Parameters["@path"].Value = file.Path; + m_InsertFileCommand.Parameters["@role"].Value = file.Role; + m_InsertFileCommand.Parameters["@size"].Value = (long)file.Size; + m_InsertFileCommand.ExecuteNonQuery(); + } + + // Insert archive contents mapping + foreach (var mapping in buildReport.fileListAssetBundleHelper.internalNameToArchiveMapping) + { + m_InsertArchiveContentCommand.Transaction = ctx.Transaction; + m_InsertArchiveContentCommand.Parameters["@build_report_id"].Value = objectId; + m_InsertArchiveContentCommand.Parameters["@assetbundle"].Value = mapping.Value; + m_InsertArchiveContentCommand.Parameters["@assetbundle_content"].Value = mapping.Key; + m_InsertArchiveContentCommand.ExecuteNonQuery(); + } + + streamDataSize = 0; + name = buildReport.Name; + } + + public void Finalize(SqliteConnection db) + { + } + + void IDisposable.Dispose() + { + m_InsertCommand?.Dispose(); + m_InsertFileCommand?.Dispose(); + m_InsertArchiveContentCommand?.Dispose(); + } +} diff --git a/Analyzer/SQLite/Handlers/MonoScriptHandler.cs b/Analyzer/SQLite/Handlers/MonoScriptHandler.cs new file mode 100644 index 0000000..6d22587 --- /dev/null +++ b/Analyzer/SQLite/Handlers/MonoScriptHandler.cs @@ -0,0 +1,49 @@ +using System; +using Microsoft.Data.Sqlite; +using UnityDataTools.Analyzer.SerializedObjects; +using UnityDataTools.FileSystem.TypeTreeReaders; + + +namespace UnityDataTools.Analyzer.SQLite.Handlers; + +public class MonoScriptHandler : ISQLiteHandler +{ + SqliteCommand m_InsertCommand; + + public void Init(SqliteConnection db) + { + using var command = db.CreateCommand(); + command.CommandText = Resources.MonoScript; + command.ExecuteNonQuery(); + + m_InsertCommand = db.CreateCommand(); + m_InsertCommand.CommandText = "INSERT INTO monoscripts(id, class_name, namespace, assembly_name) VALUES(@id, @class_name, @namespace, @assembly_name)"; + m_InsertCommand.Parameters.Add("@id", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@class_name", SqliteType.Text); + m_InsertCommand.Parameters.Add("@namespace", SqliteType.Text); + m_InsertCommand.Parameters.Add("@assembly_name", SqliteType.Text); + } + + public void Process(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize) + { + var monoScript = MonoScript.Read(reader); + m_InsertCommand.Transaction = ctx.Transaction; + m_InsertCommand.Parameters["@id"].Value = objectId; + m_InsertCommand.Parameters["@class_name"].Value = monoScript.ClassName; + m_InsertCommand.Parameters["@namespace"].Value = monoScript.Namespace; + m_InsertCommand.Parameters["@assembly_name"].Value = monoScript.AssemblyName; + m_InsertCommand.ExecuteNonQuery(); + + name = monoScript.ClassName; + streamDataSize = 0; + } + + public void Finalize(SqliteConnection db) + { + } + + void IDisposable.Dispose() + { + m_InsertCommand?.Dispose(); + } +} diff --git a/Analyzer/SQLite/Handlers/PackedAssetsHandler.cs b/Analyzer/SQLite/Handlers/PackedAssetsHandler.cs new file mode 100644 index 0000000..78268b4 --- /dev/null +++ b/Analyzer/SQLite/Handlers/PackedAssetsHandler.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using Microsoft.Data.Sqlite; +using UnityDataTools.Analyzer.SerializedObjects; +using UnityDataTools.FileSystem.TypeTreeReaders; + +namespace UnityDataTools.Analyzer.SQLite.Handlers; + +public class PackedAssetsHandler : ISQLiteHandler +{ + private SqliteCommand m_InsertPackedAssetsCommand; + private SqliteCommand m_InsertSourceAssetCommand; + private SqliteCommand m_GetSourceAssetIdCommand; + private SqliteCommand m_InsertContentsCommand; + private Dictionary<(string guid, string path), long> m_SourceAssetCache = new(); + + public void Init(SqliteConnection db) + { + using var command = db.CreateCommand(); + command.CommandText = Properties.Resources.PackedAssets ?? throw new InvalidOperationException("PackedAssets resource not found"); + command.ExecuteNonQuery(); + + m_InsertPackedAssetsCommand = db.CreateCommand(); + m_InsertPackedAssetsCommand.CommandText = @"INSERT INTO build_report_packed_assets( + id, path, file_header_size + ) VALUES( + @id, @path, @file_header_size + )"; + + m_InsertPackedAssetsCommand.Parameters.Add("@id", SqliteType.Integer); + m_InsertPackedAssetsCommand.Parameters.Add("@path", SqliteType.Text); + m_InsertPackedAssetsCommand.Parameters.Add("@file_header_size", SqliteType.Integer); + + m_InsertSourceAssetCommand = db.CreateCommand(); + m_InsertSourceAssetCommand.CommandText = @"INSERT OR IGNORE INTO build_report_source_assets( + source_asset_guid, build_time_asset_path + ) VALUES( + @source_asset_guid, @build_time_asset_path + )"; + m_InsertSourceAssetCommand.Parameters.Add("@source_asset_guid", SqliteType.Text); + m_InsertSourceAssetCommand.Parameters.Add("@build_time_asset_path", SqliteType.Text); + + m_GetSourceAssetIdCommand = db.CreateCommand(); + m_GetSourceAssetIdCommand.CommandText = @"SELECT id FROM build_report_source_assets + WHERE source_asset_guid = @source_asset_guid AND build_time_asset_path = @build_time_asset_path"; + m_GetSourceAssetIdCommand.Parameters.Add("@source_asset_guid", SqliteType.Text); + m_GetSourceAssetIdCommand.Parameters.Add("@build_time_asset_path", SqliteType.Text); + + m_InsertContentsCommand = db.CreateCommand(); + m_InsertContentsCommand.CommandText = @"INSERT INTO build_report_packed_asset_info( + packed_assets_id, object_id, type, size, offset, source_asset_id + ) VALUES( + @packed_assets_id, @object_id, @type, @size, @offset, @source_asset_id + )"; + + m_InsertContentsCommand.Parameters.Add("@packed_assets_id", SqliteType.Integer); + m_InsertContentsCommand.Parameters.Add("@object_id", SqliteType.Integer); + m_InsertContentsCommand.Parameters.Add("@type", SqliteType.Integer); + m_InsertContentsCommand.Parameters.Add("@size", SqliteType.Integer); + m_InsertContentsCommand.Parameters.Add("@offset", SqliteType.Integer); + m_InsertContentsCommand.Parameters.Add("@source_asset_id", SqliteType.Integer); + } + + public void Process(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize) + { + var packedAssets = PackedAssets.Read(reader); + + m_InsertPackedAssetsCommand.Transaction = ctx.Transaction; + m_InsertPackedAssetsCommand.Parameters["@id"].Value = objectId; + m_InsertPackedAssetsCommand.Parameters["@path"].Value = packedAssets.Path; + m_InsertPackedAssetsCommand.Parameters["@file_header_size"].Value = (long)packedAssets.FileHeaderSize; + m_InsertPackedAssetsCommand.ExecuteNonQuery(); + + // Insert contents + foreach (var content in packedAssets.Contents) + { + // Get or create source asset ID + var cacheKey = (content.SourceAssetGUID, content.BuildTimeAssetPath); + if (!m_SourceAssetCache.TryGetValue(cacheKey, out long sourceAssetId)) + { + // Insert the source asset (will be ignored if it already exists) + m_InsertSourceAssetCommand.Transaction = ctx.Transaction; + m_InsertSourceAssetCommand.Parameters["@source_asset_guid"].Value = content.SourceAssetGUID; + m_InsertSourceAssetCommand.Parameters["@build_time_asset_path"].Value = content.BuildTimeAssetPath; + m_InsertSourceAssetCommand.ExecuteNonQuery(); + + // Get the ID (whether just inserted or already existing) + m_GetSourceAssetIdCommand.Transaction = ctx.Transaction; + m_GetSourceAssetIdCommand.Parameters["@source_asset_guid"].Value = content.SourceAssetGUID; + m_GetSourceAssetIdCommand.Parameters["@build_time_asset_path"].Value = content.BuildTimeAssetPath; + sourceAssetId = (long)m_GetSourceAssetIdCommand.ExecuteScalar(); + + m_SourceAssetCache[cacheKey] = sourceAssetId; + } + + m_InsertContentsCommand.Transaction = ctx.Transaction; + m_InsertContentsCommand.Parameters["@packed_assets_id"].Value = objectId; + m_InsertContentsCommand.Parameters["@object_id"].Value = content.ObjectID; + + // TODO: Ideally we would also populate the type table if the content.Type is + // not already in that table, and if we have a string value for it in TypeIdRegistry. That would + // make it possible to view object types as strings, for the most common types, when importing a BuildReport + // without the associated built content. + m_InsertContentsCommand.Parameters["@type"].Value = content.Type; + m_InsertContentsCommand.Parameters["@size"].Value = (long)content.Size; + m_InsertContentsCommand.Parameters["@offset"].Value = (long)content.Offset; + m_InsertContentsCommand.Parameters["@source_asset_id"].Value = sourceAssetId; + m_InsertContentsCommand.ExecuteNonQuery(); + } + + streamDataSize = 0; + name = packedAssets.Path; + } + + public void Finalize(SqliteConnection db) + { + } + + void IDisposable.Dispose() + { + m_InsertPackedAssetsCommand?.Dispose(); + m_InsertSourceAssetCommand?.Dispose(); + m_GetSourceAssetIdCommand?.Dispose(); + m_InsertContentsCommand?.Dispose(); + } +} + diff --git a/Analyzer/SQLite/Parsers/Models/BuildLayout.cs b/Analyzer/SQLite/Parsers/Models/BuildLayout.cs index b18af84..16d7781 100644 --- a/Analyzer/SQLite/Parsers/Models/BuildLayout.cs +++ b/Analyzer/SQLite/Parsers/Models/BuildLayout.cs @@ -126,7 +126,7 @@ public class ReferenceData public AssetObject[] Objects; // Array of object data public ReferenceId[] ReferencingAssets; - // For BuildLayout/File + // For BuildLayout/File public ReferenceId[] Assets; public BundleObjectInfo BundleObjectInfo; // Object with Size property public ReferenceId[] ExternalReferences; diff --git a/Analyzer/SQLite/Parsers/SerializedFileParser.cs b/Analyzer/SQLite/Parsers/SerializedFileParser.cs index a7fc071..acd1658 100644 --- a/Analyzer/SQLite/Parsers/SerializedFileParser.cs +++ b/Analyzer/SQLite/Parsers/SerializedFileParser.cs @@ -64,7 +64,7 @@ bool ShouldIgnoreFile(string file) private static readonly HashSet IgnoredExtensions = new() { ".txt", ".resS", ".resource", ".json", ".dll", ".pdb", ".exe", ".manifest", ".entities", ".entityheader", - ".ini", ".config", ".hash" + ".ini", ".config", ".hash", ".md" }; bool ProcessFile(string file, string rootDirectory) diff --git a/Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs b/Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs index fdbda66..f91bcd4 100644 --- a/Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs +++ b/Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs @@ -37,6 +37,9 @@ public class SerializedFileSQLiteWriter : IDisposable { "AnimationClip", new AnimationClipHandler() }, { "AssetBundle", new AssetBundleHandler() }, { "PreloadData", new PreloadDataHandler() }, + { "MonoScript", new MonoScriptHandler() }, + { "BuildReport", new BuildReportHandler() }, + { "PackedAssets", new PackedAssetsHandler() }, }; // serialized files diff --git a/Analyzer/SerializedObjects/BuildReport.cs b/Analyzer/SerializedObjects/BuildReport.cs new file mode 100644 index 0000000..6dd413d --- /dev/null +++ b/Analyzer/SerializedObjects/BuildReport.cs @@ -0,0 +1,213 @@ +using System; +using System.Collections.Generic; +using System.IO; +using UnityDataTools.Analyzer.Util; +using UnityDataTools.FileSystem.TypeTreeReaders; + +namespace UnityDataTools.Analyzer.SerializedObjects; + +public class BuildReport +{ + public string Name { get; init; } + public string BuildGuid { get; init; } + public string PlatformName { get; init; } + public int Subtarget { get; init; } + public string StartTime { get; init; } + public string EndTime { get; init; } + public int Options { get; init; } + public int AssetBundleOptions { get; init; } + public string OutputPath { get; init; } + public uint Crc { get; init; } + public ulong TotalSize { get; init; } + public int TotalTimeSeconds { get; init; } + public int TotalErrors { get; init; } + public int TotalWarnings { get; init; } + public int BuildType { get; init; } + public string BuildResult { get; init; } + public List Files { get; init; } + public FileListAssetBundleHelper fileListAssetBundleHelper { get; init; } + + private BuildReport() { } + + public static BuildReport Read(RandomAccessReader reader) + { + var summary = reader["m_Summary"]; + + // Read the GUID (4 unsigned ints) + // Unity's GUID format reverses nibbles within each uint32 + var guidData = summary["buildGUID"]; + var guid0 = guidData["data[0]"].GetValue(); + var guid1 = guidData["data[1]"].GetValue(); + var guid2 = guidData["data[2]"].GetValue(); + var guid3 = guidData["data[3]"].GetValue(); + var guidString = GuidHelper.FormatUnityGuid(guid0, guid1, guid2, guid3); + + // Convert build start time from ticks to ISO 8601 UTC format + var startTimeTicks = summary["buildStartTime"]["ticks"].GetValue(); + var startTime = new DateTime(startTimeTicks, DateTimeKind.Utc).ToString("o"); + + // Convert ticks to seconds (TimeSpan.TicksPerSecond = 10,000,000) + var totalTimeTicks = summary["totalTimeTicks"].GetValue(); + var totalTimeSeconds = (int)Math.Round(totalTimeTicks / 10000000.0); + + var endTime = new DateTime(startTimeTicks + (long)totalTimeTicks, DateTimeKind.Utc).ToString("o"); + + // Read the files array + var filesList = new List(reader["m_Files"].GetArraySize()); + foreach (var fileElement in reader["m_Files"]) + { + filesList.Add(new BuildFile + { + Id = fileElement["id"].GetValue(), + Path = fileElement["path"].GetValue(), + Role = fileElement["role"].GetValue(), + Size = fileElement["totalSize"].GetValue() + }); + } + + TrimCommonPathPrefix(filesList); + + return new BuildReport() + { + Name = reader["m_Name"].GetValue(), + BuildGuid = guidString, + PlatformName = summary["platformName"].GetValue(), + Subtarget = summary["subtarget"].GetValue(), + StartTime = startTime, + EndTime = endTime, + Options = summary["options"].GetValue(), + AssetBundleOptions = summary["assetBundleOptions"].GetValue(), + OutputPath = summary["outputPath"].GetValue(), + Crc = summary["crc"].GetValue(), + TotalSize = summary["totalSize"].GetValue(), + TotalTimeSeconds = totalTimeSeconds, + TotalErrors = summary["totalErrors"].GetValue(), + TotalWarnings = summary["totalWarnings"].GetValue(), + BuildType = summary["buildType"].GetValue(), + BuildResult = GetBuildResultString(summary["buildResult"].GetValue()), + Files = filesList, + fileListAssetBundleHelper = new FileListAssetBundleHelper(filesList) + }; + } + + // Currently the file list has the absolute paths of the build output, but what we really want is the relative path. + // This code reuses the approach taken in the build report inspector of automatically stripping off whatever part of the path + // is repeated as the prefix on each file, which effectively finds the relative output path. + static void TrimCommonPathPrefix(List files) + { + if (files.Count == 0) + return; + string longestCommonRoot = files[0].Path; + foreach (var file in files) + { + for (var i = 0; i < longestCommonRoot.Length && i < file.Path.Length; i++) + { + if (longestCommonRoot[i] == file.Path[i]) + continue; + longestCommonRoot = longestCommonRoot.Substring(0, i); + } + } + + foreach (var file in files) + { + file.Path = file.Path.Substring(longestCommonRoot.Length); + } + } + + public static string GetBuildTypeString(int buildType) + { + return buildType switch + { + 1 => "Player", + 2 => "AssetBundle", + 3 => "ContentDirectory", + _ => buildType.ToString() + }; + } + + public static string GetBuildResultString(int buildResult) + { + return buildResult switch + { + 0 => "Unknown", + 1 => "Succeeded", + 2 => "Failed", + 3 => "Cancelled", + 4 => "Pending", + _ => buildResult.ToString() + }; + } +} + +public class BuildFile +{ + public uint Id { get; init; } + public string Path { get; set; } + public string Role { get; init; } + public ulong Size { get; init; } +} + + +/// Helper for matching files that are inside an Unity Archive file to the file containing it. +// Currently this only applies to AssetBundle builds, which can have many output files and which use hard to understand internal file names +// like "CAB-76a378bdc9304bd3c3a82de8dd97981a". +// For compressed Player builds the PackedAssets reports the internal files, but the file list does not report the unity3d content, +// so this code will not pick up the mapping. However because there is only a single unity3d file on most platforms this is less important +public class FileListAssetBundleHelper +{ + public Dictionary internalNameToArchiveMapping = new Dictionary(); + + public FileListAssetBundleHelper(List files) + { + CalculateAssetBundleMapping(files); + } + + /// + // Map between the internal file names inside Archive files back to the Archive filename. + + /* + Example input: + + - path: C:/Src/TestProject/Build/AssetBundles/audio.bundle/CAB-76a378bdc9304bd3c3a82de8dd97981a.resource + role: StreamingResourceFile + ... + - path: C:/Src/TestProject/Build/AssetBundles/audio.bundle + role: AssetBundle + ... + + Result: + CAB-76a378bdc9304bd3c3a82de8dd97981a.resource -> audio.bundle + */ + /// + private void CalculateAssetBundleMapping(List files) + { + internalNameToArchiveMapping.Clear(); + + // Track archive paths and their base filenames for AssetBundle or manifest files + var archivePathToFileName = new Dictionary(); + foreach (var file in files) + { + if (file.Role == "AssetBundle" || file.Role == "ManifestAssetBundle") + { + var justFileName = Path.GetFileName(file.Path); + archivePathToFileName[file.Path] = justFileName; + } + } + + if (archivePathToFileName.Count == 0) + return; + + // Map internal file names to their corresponding archive filenames + foreach (var file in files) + { + // Assumes internal files are not in subdirectories inside the archive + var justPath = Path.GetDirectoryName(file.Path)?.Replace('\\', '/'); + var justFileName = Path.GetFileName(file.Path); + + if (!string.IsNullOrEmpty(justPath) && archivePathToFileName.ContainsKey(justPath)) + { + internalNameToArchiveMapping[justFileName] = archivePathToFileName[justPath]; + } + } + } +} diff --git a/Analyzer/SerializedObjects/MonoScript.cs b/Analyzer/SerializedObjects/MonoScript.cs new file mode 100644 index 0000000..379bd22 --- /dev/null +++ b/Analyzer/SerializedObjects/MonoScript.cs @@ -0,0 +1,22 @@ +using UnityDataTools.FileSystem.TypeTreeReaders; + +namespace UnityDataTools.Analyzer.SerializedObjects; + +public class MonoScript +{ + public string ClassName { get; init; } + public string Namespace { get; init; } + public string AssemblyName { get; init; } + + private MonoScript() { } + + public static MonoScript Read(RandomAccessReader reader) + { + return new MonoScript() + { + ClassName = reader["m_ClassName"].GetValue(), + Namespace = reader["m_Namespace"].GetValue(), + AssemblyName = reader["m_AssemblyName"].GetValue() + }; + } +} diff --git a/Analyzer/SerializedObjects/PackedAssets.cs b/Analyzer/SerializedObjects/PackedAssets.cs new file mode 100644 index 0000000..be7741a --- /dev/null +++ b/Analyzer/SerializedObjects/PackedAssets.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using UnityDataTools.Analyzer.Util; +using UnityDataTools.FileSystem.TypeTreeReaders; + +namespace UnityDataTools.Analyzer.SerializedObjects; + +public class PackedAssets +{ + public string Path { get; init; } + public ulong FileHeaderSize { get; init; } + public List Contents { get; init; } + + private PackedAssets() { } + + public static PackedAssets Read(RandomAccessReader reader) + { + var path = reader["m_ShortPath"].GetValue(); + var fileHeaderSize = reader["m_Overhead"].GetValue(); + + var contentsList = new List(reader["m_Contents"].GetArraySize()); + + foreach (var element in reader["m_Contents"]) + { + // Read GUID (4 unsigned ints) + var guidData = element["sourceAssetGUID"]; + var guid0 = guidData["data[0]"].GetValue(); + var guid1 = guidData["data[1]"].GetValue(); + var guid2 = guidData["data[2]"].GetValue(); + var guid3 = guidData["data[3]"].GetValue(); + var guidString = GuidHelper.FormatUnityGuid(guid0, guid1, guid2, guid3); + + contentsList.Add(new PackedAssetInfo + { + ObjectID = element["fileID"].GetValue(), + Type = element["classID"].GetValue(), + Size = element["packedSize"].GetValue(), + Offset = element["offset"].GetValue(), + SourceAssetGUID = guidString, + BuildTimeAssetPath = element["buildTimeAssetPath"].GetValue() + }); + } + + return new PackedAssets() + { + Path = path, + FileHeaderSize = fileHeaderSize, + Contents = contentsList + }; + } +} + +public class PackedAssetInfo +{ + public long ObjectID { get; init; } + public int Type { get; init; } + public ulong Size { get; init; } + public ulong Offset { get; init; } + public string SourceAssetGUID { get; init; } + public string BuildTimeAssetPath { get; init; } +} + diff --git a/Analyzer/Util/GuidHelper.cs b/Analyzer/Util/GuidHelper.cs new file mode 100644 index 0000000..f7a6bd0 --- /dev/null +++ b/Analyzer/Util/GuidHelper.cs @@ -0,0 +1,45 @@ +namespace UnityDataTools.Analyzer.Util; + +/// +/// Helper class for converting Unity GUID data to string format. +/// +public static class GuidHelper +{ + /// + /// Converts Unity GUID data array to string format matching Unity's GUIDToString function. + /// Unity stores GUIDs as 4 uint32 values and converts them to a 32-character hex string + /// with a specific byte ordering that differs from standard GUID/UUID formatting. + /// + /// + /// data[0]=3856716653 (0xe60765cd) becomes "d63d0e5e" + /// + public static string FormatUnityGuid(uint data0, uint data1, uint data2, uint data3) + { + char[] result = new char[32]; + FormatUInt32Reversed(data0, result, 0); + FormatUInt32Reversed(data1, result, 8); + FormatUInt32Reversed(data2, result, 16); + FormatUInt32Reversed(data3, result, 24); + return new string(result); + } + + /// + /// Formats a uint32 as 8 hex digits matching Unity's GUIDToString logic. + /// Unity's implementation extracts nibbles from most significant to least significant + /// (j=7 down to j=0) and writes them to output positions in the same order (offset+7 to offset+0), + /// which reverses the byte order compared to standard hex formatting. + /// + /// + /// For example: 0xe60765cd becomes "d63d0e5e" (bytes reversed: cd,65,07,e6 → e6,07,65,cd) + /// + private static void FormatUInt32Reversed(uint value, char[] output, int offset) + { + const string hexChars = "0123456789abcdef"; + for (int j = 7; j >= 0; j--) + { + uint nibble = (value >> (j * 4)) & 0xF; + output[offset + j] = hexChars[(int)nibble]; + } + } +} + diff --git a/Documentation/Diagnostics-TextBasedBuildReport.png b/Documentation/Diagnostics-TextBasedBuildReport.png new file mode 100644 index 0000000..a21c76e Binary files /dev/null and b/Documentation/Diagnostics-TextBasedBuildReport.png differ diff --git a/Documentation/analyze-examples.md b/Documentation/analyze-examples.md index e3dd6f2..23f5551 100644 --- a/Documentation/analyze-examples.md +++ b/Documentation/analyze-examples.md @@ -2,9 +2,9 @@ This topic gives some examples of using the SQLite output of the UnityDataTools Analyze command. -The command line arguments to invoke Analyze are documented [here](../UnityDataTool/README.md#analyzeanalyse). +The command line arguments to invoke Analyze are documented [here](unitydatatool.md#analyzeanalyse). -The definition of the views, and some internal details about how Analyze is implemented, can be found [here](../Analyzer/README.md). +The definition of the views, and some internal details about how Analyze is implemented, can be found [here](analyzer.md). ## Running Queries from the Command line @@ -64,6 +64,9 @@ Universal Render Pipeline/Lit 115.5 KB 1b2fdfe013c58ffd57d7663 Shader Graphs/CustomLightingBuildingsB 113.4 KB 1b2fdfe013c58ffd57d7663eb8db3e60 ``` +## BuildReport support + +See [buildreport.md](buildreport.md) for information about using analyze to look at BuildReport files. ## Example: Using AI tools to help write queries @@ -100,41 +103,59 @@ Note: Both MonoBehaviours and ScriptableObjects have the same serialized type "M The previous example shows how to find all MonoBehaviours and ScriptableObjects. But you may want to filter this based on the actual scripting class. This is a bit more involved than the previous examples, so lets first breakdown the approach. -The serialized data for scripting class does not directly sort the class name, instead it stores a reference to a MonoScript. The MonoScript in turn records the assembly, namespace and classname. +The serialized data for scripting class does not directly store the class name, instead it stores a reference to a MonoScript. The MonoScript in turn records the assembly, namespace and classname. This is an example MonoScript from a `UnityDataTool dump` of a Serialized File: ``` ID: -5763254701832525334 (ClassID: 115) MonoScript - m_Name (string) ReferencedUnityObjects + m_Name (string) SpriteSkin m_ExecutionOrder (int) 0 m_PropertiesHash (Hash128) ... - m_ClassName (string) ReferencedUnityObjects - m_Namespace (string) Unity.Scenes - m_AssemblyName (string) Unity.Scenes + m_ClassName (string) SpriteSkin + m_Namespace (string) UnityEngine.U2D.Animation + m_AssemblyName (string) Unity.2D.Animation.Runtime ``` -Currently UnityDataTool does not implement custom handling for MonoScript objects, so the ClassName, Namespace and AssemblyName fields are not in the database. However the main object table records the m_Name field of object, and for a MonoScript that should match the m_Classname. For the common case, where the class name is itself unique in a project, it is possible to use the name field as the way to identify instances of the script. - -For example to list all distinct class names in the build you can run this query +UnityDataTool extracts MonoScript information into the `monoscripts` table and `monoscript_view`, which provide the class name, namespace, and assembly name for each script. This makes it easy to list all scripting classes in the build: ``` -SELECT DISTINCT name FROM object_view WHERE type = 'MonoScript'; +SELECT class_name, namespace, assembly_name FROM monoscript_view; ``` The actual scripting objects of that type may be spread all through your AssetBundles (or Player build). To find them we need to make use of the `refs` table, which records the references from each object to other objects. If we find each MonoBehaviour object that references the MonoScript with the desired class name then we have found all instances of that class. -For example, to search for all instances of the class ReferencedUnityObjects we could run this query: +The `script_object_view` provides a convenient way to query MonoBehaviour objects along with their associated script information. This view joins MonoBehaviour objects with their referenced MonoScript, bringing the class_name, namespace, and assembly_name into each row. + +For example, to search for all instances of the class SpriteSkin in the UnityEngine.U2D.Animation namespace, you can simply query: + +``` +SELECT asset_bundle, serialized_file, name, object_id, class_name, namespace, assembly_name +FROM script_object_view +WHERE class_name = 'SpriteSkin' + AND namespace = 'UnityEngine.U2D.Animation'; +``` + +If the class name is unique in your project, you can simplify the query by omitting the namespace filter: + +``` +SELECT asset_bundle, serialized_file, name, object_id, class_name, namespace +FROM script_object_view +WHERE class_name = 'SpriteSkin'; +``` + +Alternatively, you can write the query manually using the underlying tables: ``` SELECT mb.asset_bundle, mb.serialized_file, mb.name, mb.object_id FROM object_view mb INNER JOIN refs r ON mb.id = r.object -INNER JOIN objects ms ON r.referenced_object = ms.id -WHERE mb.type = 'MonoBehaviour' +INNER JOIN monoscript_view ms ON r.referenced_object = ms.id +WHERE mb.type = 'MonoBehaviour' AND r.property_type = 'MonoScript' - AND ms.name = 'ReferencedUnityObjects'; + AND ms.class_name = 'SpriteSkin' + AND ms.namespace = 'UnityEngine.U2D.Animation'; ``` ## Example: Quick summary for individual AssetBundles diff --git a/Documentation/analyzer.md b/Documentation/analyzer.md new file mode 100644 index 0000000..2e95ed3 --- /dev/null +++ b/Documentation/analyzer.md @@ -0,0 +1,222 @@ +# Analyzer + +The Analyzer is a class library that can be used to analyze the content of Unity data files such +as AssetBundles and SerializedFiles. It iterates through all the serialized objects and uses the +TypeTree to extract information about these objects (e.g. name, size, etc.) + +The most common use of this library is through the [analyze](unitydatatool.md#analyzeanalyse) +command of the UnityDataTool. This uses the Analyze library to generate a SQLite database. + +Once generated, a tool such as the [DB Browser for SQLite](https://sqlitebrowser.org/), or the command line `sqlite3` tool, can be used to look at the content of the database. + +# Example usage + +See [this topic](analyze-examples.md) for examples of how to use the SQLite output of the UnityDataTool Analyze command. + +# DataBase Reference + +The database provides different views. The views join multiple tables together and often it is not necessary to write your own SQL queries to find the information you want, especially when you are using a visual SQLite tool. + +This section gives an overview of the main views. + +## object_view + +This is the main view where the information about all the objects in the AssetBundles is available. +Its columns are: +* id: a unique id without any meaning outside of the database +* object_id: the Unity object id (unique inside its SerializedFile but not necessarily acros all + AssetBundles) +* asset_bundle: the name of the AssetBundle containing the object (will be null if the source file + was a SerializedFile and not an AssetBundle) +* serialized_file: the name of the SerializedFile containing the object +* type: the type of the object +* name: the name of the object, if it had one +* game_object: the id of the GameObject containing this object, if any (mostly for Components) +* size: the size of the object in bytes (e.g. 3343772) +* pretty_size: the size in an easier to read format (e.g. 3.2 MB) + +## view_breakdown_by_type + +This view lists the total number and size of the objects, aggregated by type. + +## view_potential_duplicates + +This view lists the objects that are possibly included more than once in the AssetBundles. This can +happen when an asset is referenced from multiple AssetBundles but is not assigned to one. In this +case, Unity will include the asset in all the AssetBundles with a reference to it. The +view_potential_duplicates provides the number of instances and the total size of the potentially +duplicated assets. It also lists all the AssetBundles where the asset was found. + +If the skipReferences option is used, there will be a lot of false positives in that view. Otherwise, +it should be very accurate because CRCs are used to determine if objects are identical. + +## asset_view (AssetBundleProcessor) + +This view lists all the assets that have been explicitly assigned to AssetBundles. The dependencies +that were automatically added by Unity at build time won't appear in this view. The columns are the +same as those in the *object_view* with the addition of the *asset_name* that contains the filename +of the asset. + +## asset_dependencies_view (AssetBundleProcessor) + +This view lists the dependencies of all the assets. You can filter by id or asset_name to get all +the dependencies of an asset. Conversely, filtering by dep_id will return all the assets that +depend on this object. This can be useful to figure out why an asset was included in a build. + +## monoscripts + +Show the class information for all the C# types of MonoBehaviour objects in the build output (including ScriptableObjects). + +This includes the assembly name, C# namespace and class name. + +## monoscripts_view + +This view is a convenient view for seeing which AssetBundle / SerializedFile contains each MonoScript object. + +## script_object_view + +This view lists all the MonoBehaviour and ScriptableObject objects in the build output, with their location, size and precise C# type (using the `monoscripts` and `refs` tables). This view is not populated if analyze is run with the `--skip-references` option. + +## animation_view (AnimationClipProcessor) + +This provides additional information about AnimationClips. The columns are the same as those in +the *object_view*, with the addition of: +* legacy: 1 if it's a legacy animation, 0 otherwise +* events: the number of events + +## audio_clip_view (AudioClipProcessor) + +This provides additional information about AudioClips. The columns are the same as those in +the *object_view*, with the addition of: +* bits_per_sample: number of bits per sample +* frequency: sampling frequency +* channels: number of channels +* load_type: either *Compressed in Memory*, *Decompress on Load* or *Streaming* +* format: compression format + +## mesh_view (MeshProcessor) + +This provides additional information about Meshes. The columns are the same as those in +the *object_view*, with the addition of: +* sub_meshes: the number of sub-meshes +* blend_shapes: the number of blend shapes +* bones: the number of bones +* indices: the number of vertex indices +* vertices: the number of vertices +* compression: 1 if compressed, 0 otherwise +* rw_enabled: 1 if the mesh has the *R/W Enabled* option, 0 otherwise +* vertex_size: number of bytes used by each vertex +* channels: name and type of the vertex channels + +## texture_view (Texture2DProcessor) + +This provides additional information about Texture2Ds. The columns are the same as those in +the *object_view*, with the addition of: +* width/height: texture resolution +* format: compression format +* mip_count: number of mipmaps +* rw_enabled: 1 if the mesh has the *R/W Enabled* option, 0 otherwise + +## shader_view (ShaderProcessor) + +This provides additional information about Shaders. The columns are the same as those in +the *object_view*, with the addition of: +* decompressed_size: the approximate size in bytes that this shader will need at runtime when + loaded +* sub_shaders: the number of sub-shaders +* sub_programs: the number of sub-programs (usually one per shader variant, stage and pass) +* unique_programs: the number of unique program (variants with identical programs will share the + same program in memory) +* keywords: list of all the keywords affecting the shader + +## shader_subprogram_view (ShaderProcessor) + +This view lists all the shader sub-programs and has the same columns as the *shader_view* with the +addition of: +* api: the API of the shader (e.g. DX11, Metal, GLES, etc.) +* pass: the pass number of the sub-program +* pass_name: the pass name, if available +* hw_tier: the hardware tier of the sub-program (as defined in the Graphics settings) +* shader_type: the type of shader (e.g. vertex, fragment, etc.) +* sub_program: the subprogram index for this pass and shader type +* keywords: the shader keywords specific to this sub-program + +## shader_keyword_ratios + +This view can help to determine which shader keywords are causing a large number of variants. To +understand how it works, let's define a "program" as a unique combination of shader, subshader, +hardware tier, pass number, API (DX, Metal, etc.), and shader type (vertex, fragment, etc). + +Each row of the view corresponds to a combination of one program and one of its keywords. The +columns are: + +* shader_id: the shader id +* name: the shader name +* sub_shader: the sub-shader number +* hw_tier: the hardware tier of the sub-program (as defined in the Graphics settings) +* pass: the pass number of the sub-program +* api: the API of the shader (e.g. DX11, Metal, GLES, etc.) +* pass_name: the pass name, if available +* shader_type: the type of shader (e.g. vertex, fragment, etc.) +* total_variants: total number of variants for this program. +* keyword: one of the program's keywords +* variants: number of variants including this keyword. +* ratio: variants/total_variants + +The ratio can be used to determine how a keyword affects the number of variants. When it is equal +to 0.5, it means that it is in half of the variants. Basically, that means that it is not stripped +at all because each of the program's variants has a version with and without that keyword. +Therefore, keywords with a ratio close to 0.5 are good targets for stripping. When the ratio is +close to 0 or 1, it means that the keyword is in almost none or almost all of the variants and +stripping it won't make a big difference. + +## view_breakdowns_shaders (ShaderProcessor) + +This view lists all the shaders aggregated by name. The *instances* column indicates how many time +the shader was found in the data files. It also provides the total size per shader and the list of +AssetBundles in which they were found. + +## BuildReport + +See [BuildReport.md](buildreport.md) for details of the tables and views related to analyzing BuildReport files. + +# Advanced + +## Using the library + +The [AnalyzerTool](../Analyzer/AnalyzerTool.cs) class is the API entry point. The main method is called +Analyze. It is currently hard coded to write using the [SQLiteWriter](../Analyzer/SQLite/SQLiteWriter.cs), +but this approach could be extended to add support for other outputs. + +Calling this method will recursively process the files matching the search pattern in the provided +path. It will add a row in the 'objects' table for each serialized object. This table contain basic +information such as the size and the name of the object (if it has one). + +## Extending the Library + +The extracted information is forwarded to an object implementing the [IWriter](../Analyzer/IWriter.cs) +interface. The library provides the [SQLiteWriter](../Analyzer/SQLite/SQLiteWriter.cs) implementation that +writes the data into a SQLite database. + +The core properties that apply to all Unity Objects are extracted into the `objects` table. +However much of the most useful Analyze functionality comes by virtue of the type-specific information that is extracted for +important types like Meshes, Shaders, Texture2D and AnimationClips. For example, when a Mesh object is encountered in a Serialized +File, then rows are added to both the `objects` table and the `meshes` table. The meshes table contains columns that only apply to Mesh objects, for example the number of vertices, indices, bones, and channels. The `mesh_view` is a view that joins the `objects` table with the `meshes` table, so that you can see all the properties of a Mesh object in one place. + +Each supported Unity object type follows the same pattern: +* A Handler class in the SQLite/Handlers, e.g. [MeshHandler.cs](../Analyzer/SQLite/Handler/MeshHandler.cs). +* The registration of the handler in the m_Handlers dictionary in [SerializedFileSQLiteWriter.cs](../Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs). +* SQL statements defining extra tables and views associated with the type, e.g. [Mesh.sql](../Analyzer/SQLite/Resources/Mesh.sql). +* A Reader class that uses RandomAccessReader to read properties from the serialized object, e.g. [Mesh.cs](../Analyzer/SerializedObjects/Mesh.cs). + +It would be possible to extend the Analyze library to add additional columns for the existing types, or by following the same pattern to add additional types. The [dump](unitydatatool.md#dump) feature of UnityDataTool is a useful way to see the property names and other details of the serialization for a type. Based on that information, code in the Reader class can use the RandomAccessReader to retrieve those properties to bring them into the SQLite database. + +## Supporting Other File Formats + +Another direction of possible extension is to support analyzing additional file formats, beyond Unity SerializedFiles. + +This the approach taken to analyze Addressables Build Layout files, which are JSON files using the format defined in [BuildLayout.cs](../Analyzer/SQLite/Parsers/Models/BuildLayout.cs). + +Support for another file format could be added by deriving an additional class from SQLiteWriter and implementing a class derived from ISQLiteFileParser. Then follow the existing code structure convention to add new Commands (derived from AbstractCommand) and Resource .sql files to establish additional tables in the database. + +An example of another file format that could be useful to support, as the tool evolves, are the yaml [.manifest files](https://docs.unity3d.com/Manual/assetbundles-file-format.html), generated by BuildPipeline.BuildAssetBundles(). diff --git a/Documentation/buildreport.md b/Documentation/buildreport.md new file mode 100644 index 0000000..1ce1d87 --- /dev/null +++ b/Documentation/buildreport.md @@ -0,0 +1,250 @@ +# BuildReport Support + +Unity generates a [BuildReport](https://docs.unity3d.com/ScriptReference/Build.Reporting.BuildReport.html) file for Player builds and when building AssetBundles via [BuildPipeline.BuildAssetBundles](https://docs.unity3d.com/ScriptReference/BuildPipeline.BuildAssetBundles.html). Build reports are **not** generated by the [Addressables](addressables-build-reports.md) package or Scriptable Build Pipeline. + +Build reports are written to `Library/LastBuild.buildreport` as Unity SerializedFiles (the same binary format used for build output). UnityDataTool can read this format and extract detailed build information using the same mechanisms as other Unity object types. + +## UnityDataTool Support + +Since build reports are SerializedFiles, you can use [`dump`](command-dump.md) to convert them to text format. + +The [`analyze`](command-analyze.md) command extracts build report data into dedicated database tables with custom handlers for: + +* **BuildReport** - The primary object containing build inputs and results +* **PackedAssets** - Describes the contents of each SerializedFile, .resS, or .resource file, including type, size, and source asset for each object or resource blob, enabling object-level analysis + +**Note:** PackedAssets information is not currently written for scenes in the build. + +## Examples + +These are some example queries that can be run after running analyze on a build report file. + +1. Show all successful builds recorded in the database. + +``` +SELECT * from build_reports WHERE build_result = "Succeeded" +``` + +2. Show information about the data in the build that originates from "Assets/Sprites/Snow.jpg". + +``` +SELECT * +FROM build_report_packed_asset_contents_view +WHERE build_time_asset_path like "Assets/Sprites/Snow.jpg" +``` + +3. Show the AssetBundles that contain content from "Assets/Sprites/Snow.jpg". + +``` +SELECT DISTINCT assetbundle +FROM build_report_packed_asset_contents_view +WHERE build_time_asset_path like "Assets/Sprites/Snow.jpg" +``` + +4. Show all source assets included in the build (excluding C# scripts, e.g. MonoScript objects) + +``` +SELECT build_time_asset_path from build_report_source_assets WHERE build_time_asset_path NOT LIKE "%.cs" +``` + +## Cross-Referencing with Build Output + +For comprehensive analysis, run `analyze` on both the build output **and** the matching build report file. Use a clean build to ensure PackedAssets information is fully populated. You may need to copy the build report into the build output directory so both are found by `analyze`. + +PackedAssets data provides source asset information for each object that isn't available when analyzing only the build output. Objects are listed in the same order as they appear in the output SerializedFile, .resS, or .resource file. + +### Database Relationships + +- Match `build_report_packed_assets` rows to analyzed SerializedFiles using `object_view.serialized_file` and `build_report_packed_assets.path` +- Match `build_report_packed_asset_info` entries to objects in the build output using `object_id` (local file ID) + +**Note:** build_report_packed_assets` also record .resS and .resource files. These rows will not match with the serialized_files table. + +**Note:** The source object's local file ID is not recorded in PackedAssetInfo. While you can identify the source asset (e.g., which Prefab), you cannot directly pinpoint the specific object within that asset. When needed, objects can often be distinguished by name or other properties. + +## Working with Multiple Build Reports + +Multiple build reports can be imported into the same database if their filenames differ. This enables: +- Comprehensive build history tracking +- Cross-build comparisons +- Identifying duplicated data between Player and AssetBundle builds + +See the schema sections below for guidance on writing queries that handle multiple build reports correctly. + +## Alternatives + +UnityDataTool provides low-level access to build reports. Consider these alternatives for easier or more convenient workflows: + +### BuildReportInspector Package + +View build reports in the Unity Editor using the [BuildReportInspector](https://github.com/Unity-Technologies/BuildReportInspector) package. + +### BuildReport API + +Access build report data programmatically within Unity using the BuildReport API: + +**1. Most recent build:** +Use [BuildPipeline.GetLatestReport()](https://docs.unity3d.com/ScriptReference/Build.Reporting.BuildReport.GetLatestReport.html) + +**2. Build report in Assets folder:** +Load via AssetDatabase API: + +```csharp +using UnityEditor; +using UnityEditor.Build.Reporting; +using UnityEngine; + +public class BuildReportInProjectUtility +{ + static public BuildReport LoadBuildReport(string buildReportPath) + { + var report = AssetDatabase.LoadAssetAtPath("Assets/MyBuildReport.buildReport"); + + if (report == null) + Debug.LogWarning($"Failed to load build report from {buildReportPath}"); + + return report; + } +} +``` + +**3. Build report outside Assets folder:** +For files in Library or elsewhere, use `InternalEditorUtility`: + + +```csharp +using System; +using System.IO; +using UnityEditor.Build.Reporting; +using UnityEditorInternal; +using UnityEngine; + +public class BuildReportUtility +{ + static public BuildReport LoadBuildReport(string buildReportPath) + { + if (!File.Exists(buildReportPath)) + return null; + + try + { + var objs = InternalEditorUtility.LoadSerializedFileAndForget(buildReportPath); + foreach (UnityEngine.Object obj in objs) + { + if (obj is BuildReport) + return obj as BuildReport; + } + } + catch (Exception ex) + { + Debug.LogWarning($"Failed to load build report from {buildReportPath}: {ex.Message}"); + } + return null; + } +} +``` + +### Text Format Access + +Build reports can be output in Unity's pseudo-YAML format using a diagnostic flag: + +![](Diagnostics-TextBasedBuildReport.png) + +Text files are significantly larger than binary. You can also convert to text by moving the binary file into your Unity project (assets default to text format). + +UnityDataTool's `dump` command produces a non-YAML text representation of build report contents. + +**When to use text formats:** +- Quick extraction of specific information via text processing tools (regex, YAML parsers, etc.) + +**When to use structured access:** +- Working with full structured data: use UnityDataTool's `analyze` command or Unity's BuildReport API + +### Addressables Build Reports + +The Addressables package generates `buildlayout.json` files instead of BuildReport files. While the format and schema differ, they contain similar information. See [Addressables Build Reports](addressables-build-reports.md) for details on importing these files with UnityDataTool. + +## Database Schema + +Build report data is stored in the following tables and views: + +| Name | Type | Description | +|------|------|-------------| +| `build_reports` | table | Build summary (type, result, platform, duration, etc.) | +| `build_report_files` | table | Files included in the build (path, role, size). See [BuildReport.GetFiles](https://docs.unity3d.com/ScriptReference/Build.Reporting.BuildReport.GetFiles.html) | +| `build_report_archive_contents` | table | Files inside each AssetBundle | +| `build_report_packed_assets` | table | SerializedFile, .resS, or .resource file info. See [PackedAssets](https://docs.unity3d.com/ScriptReference/Build.Reporting.PackedAssets.html) | +| `build_report_packed_asset_info` | table | Each object inside a SerializedFile (or data in .resS/.resource files). See [PackedAssetInfo](https://docs.unity3d.com/ScriptReference/Build.Reporting.PackedAssetInfo.html) | +| `build_report_source_assets` | table | Source asset GUID and path for each PackedAssetInfo reference | +| `build_report_files_view` | view | All files from all build reports | +| `build_report_packed_assets_view` | view | All PackedAssets with their BuildReport, AssetBundle, and SerializedFile | +| `build_report_packed_asset_contents_view` | view | All objects and resources tracked in build reports | + +The `build_reports` table contains primary build information. Additional tables store detailed content data. Views simplify queries by automatically joining tables, especially when working with multiple build reports. + +### Schema Overview + +Views automatically identify which build report each row belongs to, simplifying multi-report queries. To create custom queries, understand the table relationships: + +**Primary relationships:** +- `build_reports`: One row per analyzed BuildReport file, corresponding to the BuildReport object in the `objects` table via the `id` column +- `build_report_packed_assets`: Records the `id` of each PackedAssets object. Find the associated BuildReport via the shared `objects.serialized_file` value (PackedAssets are processed independently of BuildReport objects) + +**Auxiliary tables:** +- `build_report_files` and `build_report_archive_contents`: Stores the BuildReport object `id` for each row (as `build_report_id`). +- `build_report_packed_asset_info`: Stores the PackedAssets object `id` for each row (as `packed_assets_id`). +- `build_report_source_assets`: Normalized table of distinct source asset GUIDs and paths, linked via `build_report_packed_asset_info.source_asset_id` + +**Note:** BuildReport and PackedAssets objects are also linked in the `refs` table (BuildReport references PackedAssets in its appendices array), but this relationship is not used in built-in views since `refs` table population is optional. + +**Example: build_report_packed_assets_view** + +This view demonstrates key relationships: +- Finds the BuildReport object (`br_obj`) by type (1125) and shared `serialized_file` with the PackedAssets (`pa`) +- Retrieves the serialized file name from `serialized_files` table (`sf.name`) +- For AssetBundle builds, retrieves the AssetBundle name from `build_report_archive_contents` by matching BuildReport ID and PackedAssets path (`assetbundle` is NULL for Player builds) + +``` +CREATE VIEW build_report_packed_assets_view AS +SELECT + pa.id, + o.object_id, + brac.assetbundle, + pa.path, + pa.file_header_size, + br_obj.id as build_report_id, + sf.name as build_report_filename +FROM build_report_packed_assets pa +INNER JOIN objects o ON pa.id = o.id +INNER JOIN serialized_files sf ON o.serialized_file = sf.id +LEFT JOIN objects br_obj ON o.serialized_file = br_obj.serialized_file AND br_obj.type = 1125 +LEFT JOIN build_report_archive_contents brac ON br_obj.id = brac.build_report_id AND pa.path = brac.assetbundle_content; +``` + +### Column Naming + +For consistency and clarity, database columns use slightly different names than the BuildReport API: + +| Database Column | BuildReport API | Notes | +|-----------------|-----------------|-------| +| `build_report_packed_assets.path` | `PackedAssets.ShortPath` | Filename of the SerializedFile, .resS, or .resource file ("short" was redundant since only one path is recorded) | +| `build_report_packed_assets.file_header_size` | `PackedAssets.Overhead` | Size of the file header (zero for .resS and .resource files) | +| `build_report_packed_asset_info.object_id` | `PackedAssetInfo.fileID` | Local file ID of the object (renamed for consistency with `objects.object_id`) | +| `build_report_packed_asset_info.type` | `PackedAssetInfo.classID` | Unity object type (renamed for consistency with `objects.type`) | + +## Limitations + +**Duplicate filenames:** Multiple build reports cannot be imported into the same database if they share the same filename. This is a general UnityDataTool limitation that assumes unique SerializedFile names. + +**Type names:** While `build_report_packed_asset_info.type` records valid [Class IDs](https://docs.unity3d.com/Manual/ClassIDReference.html), the string type name may not exist in the `types` table. The `types` table is only populated when processing object instances (during TypeTree analysis). Analyzing both build output **and** build report together ensures types are fully populated; otherwise only numeric IDs are available. + +### Information Not Exported + +Currently, only the most useful BuildReport data is extracted to SQL. Additional data may be added as needed: + +* [Code stripping](https://docs.unity3d.com/ScriptReference/Build.Reporting.BuildReport-strippingInfo.html) appendix (IL2CPP Player builds) +* [ScenesUsingAssets](https://docs.unity3d.com/ScriptReference/Build.Reporting.BuildReport-scenesUsingAssets.html) (detailed build reports) +* `BuildReport.m_BuildSteps` array (SQL may not be ideal for this hierarchical data) +* `BuildAssetBundleInfoSet` appendix (undocumented object listing files in each AssetBundle; `build_report_archive_contents` currently derives this from the File list) +* Analytics-only appendices (unlikely to be valuable for analysis) + diff --git a/UnityDataTool/Commands/analyze.md b/Documentation/command-analyze.md similarity index 97% rename from UnityDataTool/Commands/analyze.md rename to Documentation/command-analyze.md index 15a6c31..16c4553 100644 --- a/UnityDataTool/Commands/analyze.md +++ b/Documentation/command-analyze.md @@ -60,7 +60,7 @@ The analyze command works with the following types of directories: The analysis creates a SQLite database that can be explored using tools like [DB Browser for SQLite](https://sqlitebrowser.org/) or the command line `sqlite3` tool. -**Refer to the [Analyzer documentation](../../Analyzer/README.md) for the database schema reference and information about extending this command.** +**Refer to the [Analyzer documentation](analyzer.md) for the database schema reference and information about extending this command.** --- diff --git a/UnityDataTool/Commands/archive.md b/Documentation/command-archive.md similarity index 94% rename from UnityDataTool/Commands/archive.md rename to Documentation/command-archive.md index 80e59f5..a563926 100644 --- a/UnityDataTool/Commands/archive.md +++ b/Documentation/command-archive.md @@ -58,7 +58,7 @@ contents/BuildPlayer-Scene2.sharedAssets contents/BuildPlayer-Scene2 ``` -> **Note:** The extracted files are binary SerializedFiles, not text. Use the [`dump`](dump.md) command to convert them to readable text format. +> **Note:** The extracted files are binary SerializedFiles, not text. Use the [`dump`](command-dump.md) command to convert them to readable text format. --- diff --git a/UnityDataTool/Commands/dump.md b/Documentation/command-dump.md similarity index 96% rename from UnityDataTool/Commands/dump.md rename to Documentation/command-dump.md index 398775b..d23b7f2 100644 --- a/UnityDataTool/Commands/dump.md +++ b/Documentation/command-dump.md @@ -90,7 +90,7 @@ ID: -8138362113332287275 (ClassID: 135) SphereCollider z float 0 ``` -**Refer to the [TextDumper documentation](../../TextDumper/README.md) for detailed output format explanation.** +**Refer to the [TextDumper documentation](textdumper.md) for detailed output format explanation.** --- diff --git a/UnityDataTool/Commands/find-refs.md b/Documentation/command-find-refs.md similarity index 92% rename from UnityDataTool/Commands/find-refs.md rename to Documentation/command-find-refs.md index 9f4e8fb..3e44fe2 100644 --- a/UnityDataTool/Commands/find-refs.md +++ b/Documentation/command-find-refs.md @@ -23,7 +23,7 @@ UnityDataTool find-refs [options] ## Prerequisites -This command requires a database created by the [`analyze`](analyze.md) command **without** the `--skip-references` option. +This command requires a database created by the [`analyze`](command-analyze.md) command **without** the `--skip-references` option. --- @@ -93,5 +93,5 @@ Found 1 reference chain(s). | `[Component of X (id=Y)]` | Shows the GameObject for Components | | `[Script = X]` | Shows the script name for MonoBehaviours | -**Refer to the [ReferenceFinder documentation](../../ReferenceFinder/README.md) for more details.** +**Refer to the [ReferenceFinder documentation](referencefinder.md) for more details.** diff --git a/Documentation/command-serialized-file.md b/Documentation/command-serialized-file.md new file mode 100644 index 0000000..ebae339 --- /dev/null +++ b/Documentation/command-serialized-file.md @@ -0,0 +1,181 @@ +# serialized-file Command + +The `serialized-file` command (alias: `sf`) provides utilities for quickly inspecting SerializedFile metadata without performing a full analysis. + +## Sub-Commands + +| Sub-Command | Description | +|-------------|-------------| +| [`externalrefs`](#externalrefs) | List external file references | +| [`objectlist`](#objectlist) | List all objects in the file | + +--- + +## externalrefs + +Lists the external file references (dependencies) in a SerializedFile. This shows which other files the SerializedFile depends on. + +### Quick Reference + +``` +UnityDataTool serialized-file externalrefs [options] +UnityDataTool sf externalrefs [options] +``` + +| Option | Description | Default | +|--------|-------------|---------| +| `` | Path to the SerializedFile | *(required)* | +| `-f, --format ` | Output format: `Text` or `Json` | `Text` | + +### Example - Text Output + +```bash +UnityDataTool serialized-file externalrefs level0 +``` + +**Output:** +``` +Index: 1, Path: globalgamemanagers.assets +Index: 2, Path: sharedassets0.assets +Index: 3, Path: Library/unity default resources +``` + +### Example - JSON Output + +```bash +UnityDataTool sf externalrefs sharedassets0.assets --format json +``` + +**Output:** +```json +[ + { + "index": 1, + "path": "globalgamemanagers.assets", + "guid": "00000000000000000000000000000000", + "type": "NonAssetType" + }, + { + "index": 2, + "path": "Library/unity default resources", + "guid": "0000000000000000e000000000000000", + "type": "NonAssetType" + } +] +``` + +--- + +## objectlist + +Lists all objects contained in a SerializedFile, showing their IDs, types, offsets, and sizes. + +### Quick Reference + +``` +UnityDataTool serialized-file objectlist [options] +UnityDataTool sf objectlist [options] +``` + +| Option | Description | Default | +|--------|-------------|---------| +| `` | Path to the SerializedFile | *(required)* | +| `-f, --format ` | Output format: `Text` or `Json` | `Text` | + +### Example - Text Output + +```bash +UnityDataTool sf objectlist sharedassets0.assets +``` + +**Output:** +``` +Id Type Offset Size +------------------------------------------------------------------------------------------ +1 PreloadData 83872 49 +2 Material 83936 268 +3 Shader 84208 6964 +4 Cubemap 91184 240 +5 MonoBehaviour 91424 60 +6 MonoBehaviour 91488 72 +``` + +### Example - JSON Output + +```bash +UnityDataTool serialized-file objectlist level0 --format json +``` + +**Output:** +```json +[ + { + "id": 1, + "typeId": 1, + "typeName": "GameObject", + "offset": 4864, + "size": 132 + }, + { + "id": 2, + "typeId": 4, + "typeName": "Transform", + "offset": 5008, + "size": 104 + } +] +``` + +--- + +## Use Cases + +### Quick File Inspection + +Use `serialized-file` when you need quick information about a SerializedFile without generating a full SQLite database: + +```bash +# Check what objects are in a file +UnityDataTool sf objectlist sharedassets0.assets + +# Check file dependencies +UnityDataTool sf externalrefs level0 +``` + +### Scripting and Automation + +The JSON output format is ideal for scripts and automated processing: + +```bash +# Extract object count +UnityDataTool sf objectlist level0 -f json | jq 'length' + +# Find specific object types +UnityDataTool sf objectlist sharedassets0.assets -f json | jq '.[] | select(.typeName == "Material")' +``` + +--- + +## SerializedFile vs Archive + +When working with AssetBundles (or a compressed Player build) you need to extract the contents first (with `archive extract`), then run the `serialized-file` command on individual files in the extracted output. + +**Example workflow:** +```bash +# 1. List contents of an archive +UnityDataTool archive list scenes.bundle + +# 2. Extract the archive +UnityDataTool archive extract scenes.bundle -o extracted/ + +# 3. Inspect individual SerializedFiles +UnityDataTool sf objectlist extracted/CAB-5d40f7cad7c871cf2ad2af19ac542994 +``` + +--- + +## Notes + +- This command only supports extracting information from the SerializedFile header of individual files. It does not extract detailed type-specific properties. Use `analyze` for full analysis of one or more SerializedFiles. +- The command uses the same native library (UnityFileSystemApi) as other UnityDataTool commands, ensuring consistent file reading across all Unity versions. + diff --git a/Documentation/referencefinder.md b/Documentation/referencefinder.md new file mode 100644 index 0000000..9231c32 --- /dev/null +++ b/Documentation/referencefinder.md @@ -0,0 +1,76 @@ +# ReferenceFinder + +The ReferenceFinder is an experimental library that can be used to find reference +chains leading to specific objects. It can be useful to determine why an asset was included into +a build. + +## How to use + +The API consists of a single class called ReferenceFinder. It requires a database that was +previously created by the [Analyzer](analyzer.md) with extractReferences option. It takes +an object id or name as input and will find reference chains originating from a root asset to the +specified object(s). A root asset is an asset that was explicitly added to an AssetBundle at build +time. + +The ReferenceFinder has two public methods named FindReferences, one taking an object id and the +other an object name and type. They both have these additional parameters: +* databasePath (string): path of the source database. +* outputFile (string): output filename. +* findAll (bool): determines if the method should find all reference chains leading to a single + object or if it should stop at the first one. + +## How to interpret the output file + +The content of the output file looks like this: + + Reference chains to + ID: 1234 + Type: Transform + AssetBundle: asset_bundle_name + SerializedFile: CAB-353837edf22eb1c4d651c39d27a233b7 + + Found reference in: + MyPrefab.prefab + (AssetBundle = MyAssetBundle; SerializedFile = CAB-353837edf22eb1c4d651c39d27a233b7) + GameObject (id=1348) MyPrefab + ↓ m_Component.Array[0].component + RectTransform (id=721) [Component of MyPrefab (id=1348)] + ↓ m_Children.Array[9] + RectTransform (id=1285) [Component of MyButton (id=1284)] + ↓ m_GameObject + GameObject (id=1284) MyButton + ↓ m_Component.Array[3].component + MonoBehaviour (id=1288) [Script = Button] [Component of MyButton (id=1284)] + ↓ m_OnClick.m_PersistentCalls.m_Calls.Array[0].m_Target + MonoBehaviour (id=1347) [Script = MyButtonEffect] [Component of MyPrefab (id=1348)] + ↓ effectText + MonoBehaviour (id=688) [Script = TextMeshProUGUI] [Component of MyButtonText (id=588)] + ↓ m_GameObject + GameObject (id=588) MyButtonText + ↓ m_Component.Array[0].component + RectTransform (id=587) [Component of MyButtonText (id=588)] + ↓ m_Father + RectTransform (id=589) [Component of MyButtonImage (id=944)] + ↓ m_Children.Array[10] + Transform (id=1234) [Component of MyButtonEffectLayer (1) (id=938)] + ↓ m_GameObject + GameObject (id=938) MyButtonEffectLayer (1) + ↓ m_Component.Array[0].component + Transform (id=1234) [Component of MyButtonEffectLayer (1) (id=938)] + + Analyzed 266 object(s). + Found 1 reference chain(s). + +For each object matching the id or name and type provided, the output file will provide the +information related to it. In this case, it was a Transform in the AssetBundle named MyAssetBundle. +It will then list all the root objects having at least one reference chain leading to that object. +In this case, there was a prefab named MyPrefab that had a hierarchy of GameObjects where one had a +reference on the Transform. + +For each reference in the chain, the name of the property is provided. For example, we can see that +the first reference in the chain is from the m_Component.Array\[0\].component property of the +MyPrefab GameObject. This is the first item in the array of Components of the GameObject and it +points to a RectTransform. When the referenced object is a Component, the corresponding GameObject +name is also provided (in this case, it's obviously MyPrefab). When MonoBehaviour are encountered, +the name of the corresponding Script is provided too (because MonoBehaviour names are empty for +some reason). The last item in the chain is the object that was provided as input. diff --git a/Documentation/textdumper.md b/Documentation/textdumper.md new file mode 100644 index 0000000..ffb9563 --- /dev/null +++ b/Documentation/textdumper.md @@ -0,0 +1,60 @@ +# TextDumper + +The TextDumper is a class library that can be used to dump the content of a Unity data +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: +* 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. + +## How to interpret the output files + +There will be one output file per SerializedFile. Depending on the type of the input file, there can +be more than one output file (e.g. AssetBundles are archives that can contain several +SerializedFiles). + +The first lines of the output file looks like this: + + External References + path(1): "Library/unity default resources" GUID: 0000000000000000e000000000000000 Type: 0 + path(2): "Resources/unity_builtin_extra" GUID: 0000000000000000f000000000000000 Type: 0 + path(3): "archive:/CAB-35fce856128a6714740898681ea54bbe/CAB-35fce856128a6714740898681ea54bbe" GUID: 00000000000000000000000000000000 Type: 0 + +This information can be used to dereference PPtrs. A PPtr is a type used by Unity to locate and load +objects in SerializedFiles. It has two fields: +* m_FileID: The file identifier is an index in the External References list above (the number in parenthesis). It will be 0 if the asset is in the same file. +* m_PathID: The object identifier in the file. Each object in a file has a unique 64 identifier, often called a Local File Identifier (LFID). + +The string after the path is the SerializedFile name corresponding to the file identifier in +parenthesis. In the case of AssetBundles this can be the path of a file inside another AssetBundle (e.g. a path starting with "archive:". The GUID and Type are internal data used by Unity. + +The rest of the file will contain an entry similar to this one for each object in the files: + + ID: -8138362113332287275 (ClassID: 135) SphereCollider + m_GameObject PPtr + m_FileID int 0 + m_PathID SInt64 -1473921323670530447 + m_Material PPtr + m_FileID int 0 + m_PathID SInt64 0 + m_IsTrigger bool False + m_Enabled bool True + m_Radius float 0.5 + m_Center Vector3f + x float 0 + y float 0 + z float 0 + +The first line contains the object identifier, the internal ClassID used by Unity, and the type name +corresponding to this ClassID. Note that the object identifier is guaranteed to be unique in this +file only. + +The next lines are the serialized fields of the objects. The first value is the field +name, the second is the type and the last is the value. If there is no value, it means that it is a +sub-object that is dumped on the next lines with a higher indentation level. + +Note: This tool is similar to the binary2text.exe executable that is included with Unity. However the syntax of the output is somewhat different. diff --git a/Documentation/unity-content-format.md b/Documentation/unity-content-format.md index f581e28..20f0f82 100644 --- a/Documentation/unity-content-format.md +++ b/Documentation/unity-content-format.md @@ -108,8 +108,8 @@ However in cases where you want to understand what contributes to the size your Often the source of content can be easily inferred, based on your own knowledge of your project, and the names of objects. For example the name of a Shader should be unique, and typically has a filename that closely matches the Shader name. -You can also use the [BuildReport](https://docs.unity3d.com/Documentation/ScriptReference/Build.Reporting.BuildReport.html) for Player and AssetBundle builds (excluding Addressables). The [Build Report Inspector](https://github.com/Unity-Technologies/BuildReportInspector) is a tool to aid in analyzing that data. +You can include a Unity BuildReport file when running `UnityDataTools analyze`. This will import the PackedAsset information, tracking the source asset information for each object in the build output. See [Build Reports](./build-reports.md) for more information, including alternative ways to view the build report. -For AssetBundles built by [BuildPipeline.BuildAssetBundles()](https://docs.unity3d.com/ScriptReference/BuildPipeline.BuildAssetBundles.html), there is also source information available in the .manifest files for each bundle. +`UnityDataTools analyze` can also import Addressables build layout files, which include source asset information. See [Addressable Build Reports](./addressables-build-reports.md). -Addressables builds do not produce a BuildReport or .manifest files, but it offers similar build information in the user interface. +For AssetBundles built by [BuildPipeline.BuildAssetBundles()](https://docs.unity3d.com/ScriptReference/BuildPipeline.BuildAssetBundles.html) Unity creates a .manifest file for each AssetBundle that has source information. This is a text-base format. diff --git a/Documentation/unitydatatool.md b/Documentation/unitydatatool.md new file mode 100644 index 0000000..0f34eae --- /dev/null +++ b/Documentation/unitydatatool.md @@ -0,0 +1,85 @@ +# UnityDataTool + +A command-line tool for analyzing and inspecting Unity build output—AssetBundles, Player builds, Addressables, and more. + +## Commands + +| Command | Description | +|---------|-------------| +| [`analyze`](command-analyze.md) | Extract data from Unity files into a SQLite database | +| [`dump`](command-dump.md) | Convert SerializedFiles to human-readable text | +| [`archive`](command-archive.md) | List or extract contents of Unity Archives | +| [`serialized-file`](command-serialized-file.md) | Quick inspection of SerializedFile metadata | +| [`find-refs`](command-find-refs.md) | Trace reference chains to objects *(experimental)* | + +--- + +## Quick Start + +```bash +# Show all commands +UnityDataTool --help + +# Analyze AssetBundles into SQLite database +UnityDataTool analyze /path/to/bundles -o database.db + +# Dump a file to text format +UnityDataTool dump /path/to/file.bundle -o /output/path + +# Extract archive contents +UnityDataTool archive extract file.bundle -o contents/ + +# Quick inspect SerializedFile +UnityDataTool serialized-file objectlist level0 +UnityDataTool sf externalrefs sharedassets0.assets --format json + +# Find reference chains to an object +UnityDataTool find-refs database.db -n "ObjectName" -t "Texture2D" +``` + +Use `--help` with any command for details: `UnityDataTool analyze --help` + +Use `--version` to print the tool version. + + +## Installation + +### Building + +First, build the solution as described in the [main README](../README.md#how-to-build). + +The executable will be at: +``` +UnityDataTool/bin/Release/net9.0/UnityDataTool.exe +``` + +> **Tip:** Add the directory containing `UnityDataTool.exe` to your `PATH` environment variable for easy access. + +### Mac Instructions + +On Mac, publish the project to get an executable: + +**Intel Mac:** +```bash +dotnet publish UnityDataTool -c Release -r osx-x64 -p:PublishSingleFile=true -p:UseAppHost=true +``` + +**Apple Silicon Mac:** +```bash +dotnet publish UnityDataTool -c Release -r osx-arm64 -p:PublishSingleFile=true -p:UseAppHost=true +``` + +If you see a warning about `UnityFileSystemApi.dylib` not being verified, go to **System Preferences → Security & Privacy** and allow the file. + +--- + +## Related Documentation + +| Topic | Description | +|-------|-------------| +| [Analyzer Database Reference](analyzer.md) | SQLite schema, views, and extending the analyzer | +| [TextDumper Output Format](textdumper.md) | Understanding dump output | +| [ReferenceFinder Details](referencefinder.md) | Reference chain output format | +| [Analyze Examples](analyze-examples.md) | Practical database queries | +| [Comparing Builds](comparing-builds.md) | Strategies for build comparison | +| [Unity Content Format](unity-content-format.md) | TypeTrees and file formats | diff --git a/README.md b/README.md index 50a107e..4a20384 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ The UnityDataTool is a command line tool and showcase of the UnityFileSystemApi The main purpose is for analysis of the content of Unity data files, for example AssetBundles and Player content. -The [command line tool](./UnityDataTool/README.md) runs directly on Unity data files, without requiring the Editor to be running. It covers most functionality of the Unity tools WebExtract and binary2text, with better performance. And it adds a lot of additional functionality, for example the ability to create a SQLite database for detailed analysis of build content. See [examples](./Documentation/analyze-examples.md) and [comparing builds](./Documentation/comparing-builds.md) for examples of how to use the command line tool. +The [command line tool](./Documentation/unitydatatool.md) runs directly on Unity data files, without requiring the Editor to be running. It covers most functionality of the Unity tools WebExtract and binary2text, with better performance. And it adds a lot of additional functionality, for example the ability to create a SQLite database for detailed analysis of build content. See [examples](./Documentation/analyze-examples.md) and [comparing builds](./Documentation/comparing-builds.md) for examples of how to use the command line tool. It is designed to scale for large build outputs and has been used to fine-tune big Unity-based games. @@ -15,12 +15,12 @@ The command line tool uses the UnityFileSystemApi library to access the content ## Repository content The repository contains the following items: -* [UnityDataTool](UnityDataTool/README.md): a command-line tool providing access to the Analyzer, TextDumper and other class libraries. -* [Analyzer](Analyzer/README.md): a class library that can be used to extract key information +* [UnityDataTool](Documentation/unitydatatool.md): a command-line tool providing access to the Analyzer, TextDumper and other class libraries. +* [Analyzer](Documentation/analyzer.md): a class library that can be used to extract key information from Unity data files and output it into a SQLite database. -* [TextDumper](TextDumper/README.md): a class library that can be used to dump SerializedFiles into +* [TextDumper](Documentation/textdumper.md): a class library that can be used to dump SerializedFiles into a human-readable format (similar to binary2text). -* [ReferenceFinder](ReferenceFinder/README.md): a class library that can be used to find +* [ReferenceFinder](Documentation/referencefinder.md): a class library that can be used to find reference chains from objects to other objects using a database created by the Analyzer * UnityFileSystem: source code and binaries of a .NET class library exposing the functionalities or the UnityFileSystemApi native library. @@ -61,7 +61,7 @@ The file name is as follows: On Windows, the executable is written to `UnityDataTool\bin\Release\net9.0`. Add this location to your system PATH for convenient access. -See the [command-line tool documentation](./UnityDataTool/README.md) for usage instructions. +See the [command-line tool documentation](./Documentation/unitydatatool.md) for usage instructions. ## Purpose of UnityFileSystemApi diff --git a/ReferenceFinder/README.md b/ReferenceFinder/README.md index ae387bf..64c8191 100644 --- a/ReferenceFinder/README.md +++ b/ReferenceFinder/README.md @@ -1,76 +1,3 @@ # ReferenceFinder -The ReferenceFinder is an experimental library that can be used to find reference -chains leading to specific objects. It can be useful to determine why an asset was included into -a build. - -## How to use - -The API consists of a single class called ReferenceFinder. It requires a database that was -previously created by the [Analyzer](../Analyzer/README.md) with extractReferences option. It takes -an object id or name as input and will find reference chains originating from a root asset to the -specified object(s). A root asset is an asset that was explicitly added to an AssetBundle at build -time. - -The ReferenceFinder has two public methods named FindReferences, one taking an object id and the -other an object name and type. They both have these additional parameters: -* databasePath (string): path of the source database. -* outputFile (string): output filename. -* findAll (bool): determines if the method should find all reference chains leading to a single - object or if it should stop at the first one. - -## How to interpret the output file - -The content of the output file looks like this: - - Reference chains to - ID: 1234 - Type: Transform - AssetBundle: asset_bundle_name - SerializedFile: CAB-353837edf22eb1c4d651c39d27a233b7 - - Found reference in: - MyPrefab.prefab - (AssetBundle = MyAssetBundle; SerializedFile = CAB-353837edf22eb1c4d651c39d27a233b7) - GameObject (id=1348) MyPrefab - ↓ m_Component.Array[0].component - RectTransform (id=721) [Component of MyPrefab (id=1348)] - ↓ m_Children.Array[9] - RectTransform (id=1285) [Component of MyButton (id=1284)] - ↓ m_GameObject - GameObject (id=1284) MyButton - ↓ m_Component.Array[3].component - MonoBehaviour (id=1288) [Script = Button] [Component of MyButton (id=1284)] - ↓ m_OnClick.m_PersistentCalls.m_Calls.Array[0].m_Target - MonoBehaviour (id=1347) [Script = MyButtonEffect] [Component of MyPrefab (id=1348)] - ↓ effectText - MonoBehaviour (id=688) [Script = TextMeshProUGUI] [Component of MyButtonText (id=588)] - ↓ m_GameObject - GameObject (id=588) MyButtonText - ↓ m_Component.Array[0].component - RectTransform (id=587) [Component of MyButtonText (id=588)] - ↓ m_Father - RectTransform (id=589) [Component of MyButtonImage (id=944)] - ↓ m_Children.Array[10] - Transform (id=1234) [Component of MyButtonEffectLayer (1) (id=938)] - ↓ m_GameObject - GameObject (id=938) MyButtonEffectLayer (1) - ↓ m_Component.Array[0].component - Transform (id=1234) [Component of MyButtonEffectLayer (1) (id=938)] - - Analyzed 266 object(s). - Found 1 reference chain(s). - -For each object matching the id or name and type provided, the output file will provide the -information related to it. In this case, it was a Transform in the AssetBundle named MyAssetBundle. -It will then list all the root objects having at least one reference chain leading to that object. -In this case, there was a prefab named MyPrefab that had a hierarchy of GameObjects where one had a -reference on the Transform. - -For each reference in the chain, the name of the property is provided. For example, we can see that -the first reference in the chain is from the m_Component.Array\[0\].component property of the -MyPrefab GameObject. This is the first item in the array of Components of the GameObject and it -points to a RectTransform. When the referenced object is a Component, the corresponding GameObject -name is also provided (in this case, it's obviously MyPrefab). When MonoBehaviour are encountered, -the name of the corresponding Script is provided too (because MonoBehaviour names are empty for -some reason). The last item in the chain is the object that was provided as input. +See [Documentation/referencefinder.md](../Documentation/referencefinder.md) diff --git a/TestCommon/Data/BuildReport1/LastBuild.buildreport b/TestCommon/Data/BuildReports/AssetBundle.buildreport similarity index 100% rename from TestCommon/Data/BuildReport1/LastBuild.buildreport rename to TestCommon/Data/BuildReports/AssetBundle.buildreport diff --git a/TestCommon/Data/BuildReports/Player.buildreport b/TestCommon/Data/BuildReports/Player.buildreport new file mode 100644 index 0000000..e70879e Binary files /dev/null and b/TestCommon/Data/BuildReports/Player.buildreport differ diff --git a/TestCommon/Data/BuildReports/README.md b/TestCommon/Data/BuildReports/README.md new file mode 100644 index 0000000..30714b8 --- /dev/null +++ b/TestCommon/Data/BuildReports/README.md @@ -0,0 +1,9 @@ +# Reference BuildReports + +These example files are used for testing UnityDataTool support for BuildReports. They are in the Unity binary format, copied from `Library/LastBuild.buildReport` after performing a build in Unity. + +They were output from the TestProject in the [BuildReportInspector](https://github.com/Unity-Technologies/BuildReportInspector/tree/master/TestProject). + +* **AssetBundle.buildreport** - Example report from an AssetBundle build (BuildPipeline.BuildAssetBundles). +* **Player.buildreport** - BuildReport for a Windows Player build with detailed build reporting (generated with Unity 6000.0.65f1) + diff --git a/TestCommon/Data/PlayerNoTypeTree/README.md b/TestCommon/Data/PlayerNoTypeTree/README.md new file mode 100644 index 0000000..29cbcac --- /dev/null +++ b/TestCommon/Data/PlayerNoTypeTree/README.md @@ -0,0 +1,3 @@ +This is a partial copy of the same build as PlayerWithTypeTrees, but with typetrees turned off. + +Without typetrees the information that can be retrieved is quite limited. \ No newline at end of file diff --git a/TestCommon/Data/PlayerNoTypeTree/level0 b/TestCommon/Data/PlayerNoTypeTree/level0 new file mode 100644 index 0000000..f4ffa8b Binary files /dev/null and b/TestCommon/Data/PlayerNoTypeTree/level0 differ diff --git a/TestCommon/Data/PlayerNoTypeTree/level1 b/TestCommon/Data/PlayerNoTypeTree/level1 new file mode 100644 index 0000000..3afb651 Binary files /dev/null and b/TestCommon/Data/PlayerNoTypeTree/level1 differ diff --git a/TestCommon/Data/PlayerNoTypeTree/sharedassets0.assets b/TestCommon/Data/PlayerNoTypeTree/sharedassets0.assets new file mode 100644 index 0000000..f6d798e Binary files /dev/null and b/TestCommon/Data/PlayerNoTypeTree/sharedassets0.assets differ diff --git a/TestCommon/Data/PlayerNoTypeTree/sharedassets1.assets b/TestCommon/Data/PlayerNoTypeTree/sharedassets1.assets new file mode 100644 index 0000000..dfd9413 Binary files /dev/null and b/TestCommon/Data/PlayerNoTypeTree/sharedassets1.assets differ diff --git a/TestCommon/Data/PlayerWithTypeTrees/LastBuild.buildreport b/TestCommon/Data/PlayerWithTypeTrees/LastBuild.buildreport new file mode 100644 index 0000000..e847270 Binary files /dev/null and b/TestCommon/Data/PlayerWithTypeTrees/LastBuild.buildreport differ diff --git a/TestCommon/Data/PlayerWithTypeTrees/README.md b/TestCommon/Data/PlayerWithTypeTrees/README.md new file mode 100644 index 0000000..4294679 --- /dev/null +++ b/TestCommon/Data/PlayerWithTypeTrees/README.md @@ -0,0 +1,36 @@ +# Test data description + +This is the content output of a Player build, made with Unity 6000.0.65f1. +The diagnostic switch to enable TypeTrees was enabled when the build was performed. + +The project is very simple and intended to be used for precise tests of expected content. + +## Content + +The build includes two scene files: +* SceneWithReferences.unity (level0) uses MonoBehaviours to references BasicScriptableObject.asset and Asset2.asset +* SceneWithReferences2.unity (level1) uses MonoBehaviours to reference BasicScriptableObject.asset and ScriptableObjectWithSerializeReference.asset + +Based on that sharing arrangement: +* sharedassets0.assets contains BasicScriptableObject.asset and Asset2.asset +* sharedassets1.assets contains ScriptableObjectWithSerializeReference.asset + +There are also additional content +* globalgamemanager with the preference objects +* globalgamemanager.assets with assets referenced from the globalgamemanager file +* globalgamemanagers.assets.resS containing the splash screen referenced from globalgamemanager.assets + +Note: The binaries, json files and other output that were also output from the player build are not checked in, because they are not needed by UnityDataTool. + +## BuildReport + +The LastBuild.buildreport file (created in the Library folder) has also been copied in. + +## Scripting types + +The MonoBehaviour used in level0 and level1 to reference the ScriptableObject is of type MonoBehaviourWithReference. + +* BasicScriptableObject.asset and Asset2.asset are instances of the BasicScriptableObject class. +* ScriptableObjectWithSerializeReference.asset is an instance of the MyNamespace.ScriptableObjectWithSerializeReference class. + + diff --git a/TestCommon/Data/PlayerWithTypeTrees/globalgamemanagers b/TestCommon/Data/PlayerWithTypeTrees/globalgamemanagers new file mode 100644 index 0000000..b46705a Binary files /dev/null and b/TestCommon/Data/PlayerWithTypeTrees/globalgamemanagers differ diff --git a/TestCommon/Data/PlayerWithTypeTrees/globalgamemanagers.assets b/TestCommon/Data/PlayerWithTypeTrees/globalgamemanagers.assets new file mode 100644 index 0000000..1b51c52 Binary files /dev/null and b/TestCommon/Data/PlayerWithTypeTrees/globalgamemanagers.assets differ diff --git a/TestCommon/Data/PlayerWithTypeTrees/globalgamemanagers.assets.resS b/TestCommon/Data/PlayerWithTypeTrees/globalgamemanagers.assets.resS new file mode 100644 index 0000000..39775b8 Binary files /dev/null and b/TestCommon/Data/PlayerWithTypeTrees/globalgamemanagers.assets.resS differ diff --git a/TestCommon/Data/PlayerWithTypeTrees/level0 b/TestCommon/Data/PlayerWithTypeTrees/level0 new file mode 100644 index 0000000..9afab1b Binary files /dev/null and b/TestCommon/Data/PlayerWithTypeTrees/level0 differ diff --git a/TestCommon/Data/PlayerWithTypeTrees/level1 b/TestCommon/Data/PlayerWithTypeTrees/level1 new file mode 100644 index 0000000..ff70f82 Binary files /dev/null and b/TestCommon/Data/PlayerWithTypeTrees/level1 differ diff --git a/TestCommon/Data/PlayerWithTypeTrees/sharedassets0.assets b/TestCommon/Data/PlayerWithTypeTrees/sharedassets0.assets new file mode 100644 index 0000000..979ce6a Binary files /dev/null and b/TestCommon/Data/PlayerWithTypeTrees/sharedassets0.assets differ diff --git a/TestCommon/Data/PlayerWithTypeTrees/sharedassets0.assets.resS b/TestCommon/Data/PlayerWithTypeTrees/sharedassets0.assets.resS new file mode 100644 index 0000000..84d803c --- /dev/null +++ b/TestCommon/Data/PlayerWithTypeTrees/sharedassets0.assets.resS @@ -0,0 +1 @@ + !!!!!!!!!!"""""""""""""##########$$$$$$$$$$%%%% !!!!!!!!!!"""""""""""""#########$$$$$$$$$$$%%%%% !!!!!!!!!!"""""""""""""#########$$$$$$$$$$%%%%%%% !!!!!!!!!!""""""""""""#########$$$$$$$$$$%%%%%%%%% !!!!!!!!!!""""""""""""#########$$$$$$$$$$%%%%%%%%%% !!!!!!!!!!""""""""""""#########$$$$$$$$$%%%%%%%%%%%% !!!!!!!!!""""""""""""########$$$$$$$$$$%%%%%%%%%%%&& !!!!!!!!!""""""""""""########$$$$$$$$$%%%%%%%%%%%&&&& !!!!!!!!!!"""""""""""########$$$$$$$$$%%%%%%%%%%%&&&&& !!!!!!!!!!""""""""""########$$$$$$$$$%%%%%%%%%%%&&&&&&& !!!!!!!!!!""""""""""########$$$$$$$$$%%%%%%%%%%&&&&&&&&& !!!!!!!!!!""""""""""########$$$$$$$$%%%%%%%%%%&&&&&&&&&&& !!!!!!!!!!""""""""""#######$$$$$$$$$%%%%%%%%%%&&&&&&&&&&&& !!!!!!!!!!"""""""""########$$$$$$$$%%%%%%%%%%&&&&&&&&&&&&'' !!!!!!!!!!"""""""""########$$$$$$$$%%%%%%%%%%&&&&&&&&&&&'''' !!!!!!!!!!"""""""""#######$$$$$$$$%%%%%%%%%%&&&&&&&&&&&'''''' !!!!!!!!!"""""""""#######$$$$$$$$%%%%%%%%%&&&&&&&&&&&'''''''' !!!!!!!!!""""""""########$$$$$$$$%%%%%%%%%&&&&&&&&&&'''''''''' !!!!!!!!!""""""""#######$$$$$$$$%%%%%%%%%&&&&&&&&&&&''''''''''' !!!!!!!!!""""""""#######$$$$$$$$%%%%%%%%%&&&&&&&&&&''''''''''''( !!!!!!!!""""""""########$$$$$$$%%%%%%%%%&&&&&&&&&&''''''''''''((( !!!!!!!!!""""""""#######$$$$$$$$%%%%%%%%&&&&&&&&&&''''''''''''((((( !!!!!!!!!""""""""########$$$$$$$$%%%%%%%%&&&&&&&&&&'''''''''''((((((( !!!!!!!!!!""""""""########$$$$$$$%%%%%%%%&&&&&&&&&&'''''''''''((((((((( !!!!!!!!!!""""""""########$$$$$$$$%%%%%%%%&&&&&&&&&'''''''''''((((((((()) !!!!!!!!!!!""""""""########$$$$$$$%%%%%%%%&&&&&&&&&'''''''''''((((((((())))  !!!!!!!!!!""""""""########$$$$$$$$%%%%%%%%&&&&&&&&&''''''''''(((((((()))))))  !!!!!!!!!!""""""""########$$$$$$$$%%%%%%%%&&&&&&&&&''''''''''(((((((()))))))))  !!!!!!!!!!"""""""""########$$$$$$$$%%%%%%%&&&&&&&&&'''''''''(((((((())))))))))** !!!!!!!!!!!!""""""""########$$$$$$$$%%%%%%%%&&&&&&&&'''''''''(((((((()))))))))***** !!!!!!!!!!!!!"""""""""########$$$$$$$$%%%%%%%&&&&&&&&&''''''''(((((((()))))))))*******!!!!! !!!!!!!!!!!!!"""""""""########$$$$$$$$%%%%%%%%&&&&&&&&''''''''((((((()))))))))**********!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!"""""""""########$$$$$$$$%%%%%%%%&&&&&&&&'''''''(((((((())))))))**********+++!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"""""""""#########$$$$$$$$%%%%%%%%&&&&&&&'''''''((((((()))))))))*********++++++""!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"""""""""#########$$$$$$$$%%%%%%%%&&&&&&&'''''''((((((())))))))*********+++++++++"""""""!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!""""""""""""""########$$$$$$$$%%%%%%%%&&&&&&''''''''((((((())))))))*********++++++++,,,""""""""""""""!!!!!!!!!!!!!!!!!!!!!"""""""""""""""""#########$$$$$$$$%%%%%%%&&&&&&&'''''''((((((())))))))*********++++++++,,,,,,####""""""""""""""""""""""""""""""""""""""""""""###########$$$$$$$$%%%%%%%&&&&&&&'''''''((((((()))))))*********+++++++,,,,,,,,,-##########""""""""""""""""""""""""""""""""""###########$$$$$$$$$$%%%%%%%&&&&&&&'''''''((((((()))))))********+++++++,,,,,,,,,----$#################""""""""""""""""""""##############$$$$$$$$$$%%%%%%%&&&&&&&'''''''((((((()))))))********+++++++,,,,,,,,--------$$$$$$$##########################################$$$$$$$$$$%%%%%%%%&&&&&&&'''''''((((((()))))))*******+++++++,,,,,,,,---------..$$$$$$$$$$$$$################################$$$$$$$$$$%%%%%%%%%&&&&&&&'''''''((((((()))))))*******+++++++,,,,,,,---------......%%%%$$$$$$$$$$$$$$$$$$$$$$##########$$$$$$$$$$$$$$$$%%%%%%%%%&&&&&&&&'''''''((((((())))))*******++++++,,,,,,,,---------.........%%%%%%%%%%$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$%%%%%%%%%%%&&&&&&&'''''''((((((())))))******+++++++,,,,,,,---------..........///&%%%%%%%%%%%%%%%%%$$$$$$$$$$$$$$$$$$$$$%%%%%%%%%%%%%%%%&&&&&&&&&'''''''(((((())))))******+++++++,,,,,,,--------..........///////&&&&&&&&%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%&&&&&&&&&&''''''''((((((())))))******+++++++,,,,,,,--------........./////////00&&&&&&&&&&&&&&%%%%%%%%%%%%%%%%%%%%%%%%%%&&&&&&&&&&&&&&&''''''''((((((()))))))******+++++++,,,,,,,-------........./////////000000''''''''&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&''''''''''(((((((())))))*******++++++,,,,,,,--------......./////////00000000001'''''''''''''''&&&&&&&&&&&&&&&&&&&&&&&&&&&'''''''''''''(((((((()))))))*******++++++,,,,,,,-------.......////////0000000001111111((((((('''''''''''''''''''''''''''''''''''''''''''((((((((())))))))******+++++++,,,,,,,------.......////////00000000111111111112((((((((((((((((''''''''''''''''''''''''''((((((((((((())))))))*******+++++++,,,,,,------.......///////0000000001111111112222222)))))))))(((((((((((((((((((((((((((((((((((((((())))))))))*******+++++++,,,,,,-------......///////00000000111111111222222222233**)))))))))))))))))))(((((((((((((((((())))))))))))))*********+++++++,,,,,,-------......///////000000011111111122222222233333333************))))))))))))))))))))))))))))))))))***********++++++++,,,,,,-------......///////0000000111111122222222223333333333344+++++**********************************************+++++++++,,,,,,,-------......//////000000011111112222222223333333333344444444+++++++++++++++++***********************++++++++++++++,,,,,,,,-------.......//////0000000111111122222222333333333444444444444555,,,,,,,,,+++++++++++++++++++++++++++++++++++++,,,,,,,,,,,--------......///////00000011111112222222233333333444444444445555555555---,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,---------........//////0000000111111222222233333333344444444455555555555555666------------------,,,,,,,,,,,,,,,,,,,,,--------------.........///////00000011111112222222333333344444444455555555555566666666666.........-----------------------------------............////////0000001111111222222233333334444444455555555555666666666666667777////............................................//////////0000000011111122222223333333444444455555555556666666666677777777777777//////////////////////////////////////////////////000000000111111112222222333333344444455555555566666666667777777777777788888888000000000000000000//////////////////000000000000000011111111122222223333333444444455555555666666666777777777778888888888888888891111111111111000000000000000000000000001111111111111122222222233333334444444555555556666666677777777778888888888888899999999999911111111110000000000000000000000000000000000000000000011111111111111222222222223333333333444444444455555555555555566666666666666///////////////..................------,,,,,,,,,,,,,+++++++++++++++++++++++,,,,,,,,,,,,,------............////////////0000000000**)))))))(((((((''''''&&&&&&%%%%%%$$$$##"""""""!!!!!! !!!!!""""""""##$$$%%%%%%%&&&&&&'''''((((((()))))))***""""!!!!!  !!!!""""" !!!!!""""""#####$$$$$%%% !!!!!""""""#####$$$$$%%%% !!!!!""""""#####$$$$$%%%%% !!!!""""""####$$$$$%%%%%%& !!!!!""""#####$$$$%%%%%%&&& !!!!!""""####$$$$$%%%%%&&&&& !!!!!""""####$$$$%%%%%&&&&&&' !!!!!""""####$$$$%%%%%&&&&&''' !!!!"""""####$$$%%%%%&&&&&&'''' !!!!!""""####$$$$%%%%%&&&&&'''''( !!!!""""####$$$$%%%%&&&&&'''''((( !!!!"""""###$$$$%%%%&&&&&'''''((((( !!!!!""""####$$$$%%%%&&&&'''''((((())  !!!!!""""#####$$$%%%%&&&&&''''((((()))) !!!!!"""""####$$$$%%%%&&&&''''(((()))))**!! !!!!!!!""""####$$$$%%%%&&&&''''(((()))))****!!!!!!!!!!!!!!!!!!!!!!!!"""""#####$$$%%%%&&&&''''(((())))****+++""""!!!!!!!!!!!!!!!!!""""""#####$$$$%%%%&&&'''(((()))))****++++,#"""""""""""""""""""""""#####$$$$$%%%&&&''''(((())))****+++,,,,,########"""""""""""#######$$$$$%%%%&&&''''((())))****+++,,,,,---$$$$$#################$$$$$$%%%%&&&&'''(((()))****+++,,,,,----..%%%%$$$$$$$$$$$$$$$$$$$$$%%%%&&&&''''((())))***+++,,,,----...../&&&&%%%%%%%%%%%%%%%%%%%%%&&&&&''''((()))****+++,,,,----..../////''&&&&&&&&&&&&&&%&&&&&&&&&''''(((()))****+++,,,----....////00000((''''''''''''&'&'''''''''((((()))***++++,,,----...////000001111))((((((((((((((((((((((()))))****+++,,,----...////0000111112222***))))))))))))))))))))))*****+++,,,,---...////00001112222223333++++********************+++++,,,,---...////000111122223333334444,,,,,,+++++++++++++++,,,,,,,----...///00001111222333334444445555--------,,,,,,,,,,,--------....///000011122223333344444555555555.-..------------------......///000001112222333344444455555555566--------------,-------------......//0000011111122223333333344444+++++************)))))))))))))*******+++++,,,,-----.....////////&&&%%%%%%%%$$$$########""""""""""""####$##$$$$%%%&&&&&''''((((()  ! !!!""###$$$%% !!!""###$$%%%& !!!""##$$$%%%&& !!!""##$$%%%&&&' !!!""##$$%%&&&''( !!""##$$$%%&&''((( !!!""##$$%%&&''((())!!!!!!!!!!!!!""##$$%%&&''((())**"""""!!!"""""##$$$%%&''(())***++######""#####$$%%&&''(())**++,,,%$$$$$$$$$$$%%%&&''(()**++,,,--.&&&%%%%%%%%&&&''(())**+,,,--.../'''''''''''''(())**++,,--..////0((((((((((()))**++,,--..///00000))))))))))))***++,,--...///00000(((((((((((()))***++,,,----.....%%%$$$$$$$$$$$%%%%&&'''((()))))*  !!!"""##$$ !"##$%% !!"#$$%&& !!"#$%%&'(!!!!!!"##$%&'(()"""""##$%&''()**$$$$$$%%&'()**++$$$%%%&''()**+++$$$$$%%&&'(()))*!!!!!!!""##$$%%& ! !"#$%!!!"#$&'"""#%&''!!"#$%&& !"# !#$ !#$ !          ! !! !!! !!!! !!!!!! !!!!!!! !!!!!!!!! !!!!!!!!!! ""!!!!!!!!!  """"!!!!!!!!!  """"""!!!!!!!!!  """"""""!!!!!!!!  !!!#"""""""""!!!!!!!!  !!!!!!##""""""""""!!!!!!!!  !!!!!!!!!!####""""""""""!!!!!!!!  !!!!!!!!!""""######""""""""""!!!!!!!!  !!!!!!!!!!"""""""########""""""""""!!!!!!!!  !!!!!!!!!""""""""""#$#########"""""""""!!!!!!!!  !!!!!!!!!!""""""""""####$$$$########""""""""""!!!!!!!  !!!!!!!!!""""""""""#######$$$$$$$#########"""""""""!!!!!!!!  !!!!!!!!!""""""""""########$$$$%%$$$$$$$########"""""""""!!!!!!!!!!  !!!!!!!!!""""""""""########$$$$$$%%%%%%$$$$$$$########"""""""""!!!!!!!!!!!  !!!!!!!!!!"""""""""########$$$$$$$%%%%%%%%%%%%$$$$$$$########"""""""""!!!!!!!!!!!!  !!!!!!!!!!!!"""""""""########$$$$$$$%%%%%%%&&&&%%%%%%%%$$$$$$$#######""""""""""!!!!!!!!!!!!! !!!!!!!!!!!!!""""""""""########$$$$$$$%%%%%%%&&&&&&&&&&&%%%%%%%$$$$$$$$########""""""""""!!!!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!"""""""""""#######$$$$$$$%%%%%%%%&&&&&&&'''&&&&&&&&%%%%%%%$$$$$$$$#########""""""""""!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!""""""""""""########$$$$$$$%%%%%%%&&&&&&&&''''''''''&&&&&&&&%%%%%%%$$$$$$$$$#########"""""""""""""!!!!!!!!!!!!!!!!!!!!!"""""""""""""""#########$$$$$$$%%%%%%%%&&&&&&&'''''''''(((''''''&&&&&&&%%%%%%%%%$$$$$$$$$##########""""""""""""""""""""""""""""""""""""""#########$$$$$$$$%%%%%%%%&&&&&&&&''''''''(((((((('''''''''&&&&&&&%%%%%%%%%%$$$$$$$$$##############""""""""""""""""""""""###########$$$$$$$$$%%%%%%%%&&&&&&&&''''''''((((((((())))(((''''''''&&&&&&&&&%%%%%%%%%%$$$$$$$$$$$$#################################$$$$$$$$$$%%%%%%%%%&&&&&&&&''''''''((((((((())))))))*((((((('''''''&&&&&&&&&&%%%%%%%%%%$$$$$$$$$$$$$$$$$$#############$$$$$$$$$$$$$$%%%%%%%%%&&&&&&&&&''''''''((((((((()))))))*******))((((((((''''''''&&&&&&&&&&&%%%%%%%%%%%%%%$$$$$$$$$$$$$$$$$$$$$$$$$$$%%%%%%%%%%%%&&&&&&&&&'''''''''(((((((()))))))********+++++)))))(((((((((''''''''''&&&&&&&&&&%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%&&&&&&&&&&&''''''''''((((((())))))))*******+++++++++,,))))))))))(((((((('''''''''''&&&&&&&&&&&&&&&%%%%%%%%%%%%%%%%%%%%%%&&&&&&&&&&&&&'''''''''(((((((()))))))********+++++++++,,,,,,,,*****)))))))))(((((((((('''''''''''&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&'''''''''''((((((((()))))))********++++++++,,,,,,,,,------*********))))))))))((((((((((('''''''''''''''''''&&&&''''''''''''''''''''((((((((()))))))))*******+++++++++,,,,,,,,--------.....+++++**********)))))))))))(((((((((((((('''''''''''''''''''''''(((((((((((()))))))))********++++++++,,,,,,,---------.........///++++++++++***********)))))))))))))((((((((((((((((((((((((((((((())))))))))))********++++++++,,,,,,,--------.........//////////0,,,,,+++++++++++*************)))))))))))))))))))))))))))))))))))))))*********++++++++,,,,,,,,--------......../////////0000000000,,,,,,,,,,,+++++++++++++********************************************+++++++++,,,,,,,,--------......../////////000000000011111111------,,,,,,,,,,,,++++++++++++++++++******************++++++++++++++,,,,,,,,,,--------........////////00000000111111111122222222--------------,,,,,,,,,,,,,,,,+++++++++++++++++++++++++++,,,,,,,,,,,,---------........///////00000000111111111222222222223333333.......-----------------,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,-----------........////////0000000011111111222222222233333333333344444/...............------------------------------------------...........////////000000001111111122222222333333333334444444444445555////////////...............................................//////////00000000111111122222222333333333444444444455555555555555666000000///////////////////////////......../////////////////00000000011111111222222233333333444444444555555555556666666666666667770000000000000000000000////////////////////000000000000000111111111222222233333333444444455555555556666666666667777777777777777881111111111111110000000000000000000000000001111111111111122222222233333334444444555555555666666666777777777777888888888888888888911111111111100000000000000000000000000000000000000000000111111111111112222222222233333333333444444444445555555555555555566666666////////////////.................------,,,,,,,,,,,,,++++++++++++++++++++++++,,,,,,,,,,,,,--------............/////////////000000**))))))))((((((''''''&&&&&&%%%%%%$$$$##"""""""!!!!!! !!!!!"""""""##$$$$%%%%%%%&&&&&&''''''(((((())))))))*""""!!!!!  !!!!""""     ! !! !!!! !!!!! ""!!!!!  """"!!!!!  !#"""""!!!!  !!!!!###"""""!!!!  !!!!!"""$####"""""!!!!  !!!!""""###$$$####"""""!!!!  !!!!!""""####$$%%$$$$####""""!!!!!  !!!!!"""""####$$$$%%%%%%$$$$#####""""!!!!!! !!!!!!"""""####$$$$%%%&&&&&&%%%%$$$$$####"""""!!!!!!!!!!!!!!!!!!!"""""####$$$$%%%%%&&&'''''&&&&%%%%$$$$$#####""""""""""""""""""""#####$$$$%%%%&&&&''''((((''''&&&&&%%%%$$$$$$##################$$$$$%%%%%&&&'''''(((())))(((((''''&&&&&%%%%%%$$$$$$$$$$$$$$$$$%%%%&&&&&''''((((())))****+))))((((''''''&&&&&&%%%%%%%%%%%%%%&&&&&&''''((((())))***+++++,,,***))))))(((((''''''''&&&&&&&&''''''''(((()))))***++++,,,,,-----+++******)))))((((((((((((((((((((()))))****++++,,,,----.....///,,,+++++++*******))))))))))))*******+++++,,,,----..../////000000---,,,,,,,++++++++++++++++++++++,,,,,----.....////00001111112222..---------,,,,,,,,,,,,,,,,,------....////0000011112222223333333.......-------------------.......////000011112222233333334444444.--.----------,--,-,,,---------...../////00000011112222222233333++++++***********)))))))))))))))))******+++++,,,,-----........//&&&%%%%%%%%$$$$#####""#""""""""""""#"######$$$$%%%%&&&''''''((((   ! !! "!!!  """!!  !!##"""!!  !!"""$$##"""!!  !!!""##$$%%$$###""!!!! !!!!""###$$%%&&&%%%$$###"""""""""###$$$%%&&''''''&&&%%%$$$$$$$$$$%%%&&'''(()))(((('''&&&&&&%&&&&&'''(())***+++))))(((((''''''(((())***+++,,,,-)))))))(((((())))***+++,,,,-----(((((((((((((((()))****++++,,,,,%%%%%$$$$$$$$$$$$%%%%&&&''''((((!  !!!"""# ! "!  #"!!  !""$##""!!!!!!""#$$%%$$###"###$%%&&%%%%$$$$%%%&&'''$$$$$$$$$%%&&&&&"!!! !!!"""###! "!  !#"!!!!""""!!"""# !  !!!!!!!!!!!!!!!!!!!!"""""""""""""""########################$$$$$$$$$$$$$$$$$$%%%% !!!!!!!!!!!!!!!!!!!"""""""""""""""#########################$$$$$$$$$$$$$$$$$%%% !!!!!!!!!!!!!!!!!!!"""""""""""""""#########################$$$$$$$$$$$$$$$$$% !!!!!!!!!!!!!!!!!!""""""""""""""""########################$$$$$$$$$$$$$$$$$ !!!!!!!!!!!!!!!!!!""""""""""""""""########################$$$$$$$$$$$$$$$ !!!!!!!!!!!!!!!!!"""""""""""""""""########################$$$$$$$$$$$$$ !!!!!!!!!!!!!!!!""""""""""""""""""#######################$$$$$$$$$$$$ !!!!!!!!!!!!!!!!""""""""""""""""""#######################$$$$$$$$$$ !!!!!!!!!!!!!!!!"""""""""""""""""""#######################$$$$$$$$ !!!!!!!!!!!!!!!!"""""""""""""""""""######################$$$$$$$ !!!!!!!!!!!!!!!!"""""""""""""""""""#######################$$$$$ !!!!!!!!!!!!!!!""""""""""""""""""""######################$$$$ !!!!!!!!!!!!!!!!"""""""""""""""""""######################$$ !!!!!!!!!!!!!!!!!"""""""""""""""""""###################### !!!!!!!!!!!!!!!!""""""""""""""""""""#################### !!!!!!!!!!!!!!!!""""""""""""""""""""################## !!!!!!!!!!!!!!!!"""""""""""""""""""""################ !!!!!!!!!!!!!!!!""""""""""""""""""""""############# !!!!!!!!!!!!!!!!""""""""""""""""""""""########### !!!!!!!!!!!!!!!!"""""""""""""""""""""""######### !!!!!!!!!!!!!!!!"""""""""""""""""""""""####### !!!!!!!!!!!!!!!""""""""""""""""""""""""##### !!!!!!!!!!!!!!!!""""""""""""""""""""""""### !!!!!!!!!!!!!!!!""""""""""""""""""""""""" !!!!!!!!!!!!!!!!""""""""""""""""""""""" !!!!!!!!!!!!!!!!""""""""""""""""""""" !!!!!!!!!!!!!!!!!""""""""""""""""""" !!!!!!!!!!!!!!!!!""""""""""""""""" !!!!!!!!!!!!!!!!!!""""""""""""""" !!!!!!!!!!!!!!!!!!""""""""""""" !!!!!!!!!!!!!!!!!!""""""""""" !!!!!!!!!!!!!!!!!!""""""""" !!!!!!!!!!!!!!!!!!""""""" !!!!!!!!!!!!!!!!!!!""""" !!!!!!!!!!!!!!!!!!!""" !!!!!!!!!!!!!!!!!!!" !!!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!! !!!!!!!!!!!!!!! !!!!!!!!!!!!! !!!!!!!!!!! !!!!!!!!! !!!!!!!! !!!!!! !!!!! !!! !          !!!!!!!!!!"""""""###########$$$$$$$$$$%% !!!!!!!!!""""""""###########$$$$$$$$$$ !!!!!!!!!""""""""###########$$$$$$$$ !!!!!!!!"""""""""###########$$$$$$ !!!!!!!"""""""""############$$$$ !!!!!!!!""""""""""##########$$$ !!!!!!!""""""""""###########$ !!!!!!!!""""""""""########## !!!!!!!!""""""""""######## !!!!!!!!!""""""""""###### !!!!!!!!""""""""""""### !!!!!!!!""""""""""""# !!!!!!!!!""""""""""" !!!!!!!!!""""""""" !!!!!!!!!""""""" !!!!!!!!!""""" !!!!!!!!!""" !!!!!!!!!!" !!!!!!!!! !!!!!!! !!!!! !!! !      !!!!!""""#####$$$$$%% !!!!!""""#####$$$$$ !!!!"""""#####$$$ !!!!"""""#####$ !!!!""""##### !!!!"""""### !!!!"""""# !!!!"""" !!!!!"" !!!!! !!! !    !!"""##$$$% !!""###$$ !!""### !!""# !!"" !! !  !!""#$$ !""# !" !  !"# ! !"    !!!!  !!!!!!!!  "!!!!!!!!!!!  """""!!!!!!!!!!!  """""""""!!!!!!!!!!!  !!!#"""""""""""""!!!!!!!!!!  !!!!!!######""""""""""""!!!!!!!!!!!  !!!!!!!!!!$#########"""""""""""""!!!!!!!!!!!  !!!!!!!!!!!!""$$$$$###########""""""""""""!!!!!!!!!!!!  !!!!!!!!!!!!!"""""%%$$$$$$$$###########""""""""""""!!!!!!!!!!!!!  !!!!!!!!!!!!!!"""""""""%%%%%%%$$$$$$$$############""""""""""""!!!!!!!!!!!!!! !!!!!!!!!!!!!!!"""""""""""###&&&%%%%%%%%%$$$$$$$$#############"""""""""""""!!!!!!!!!!!!!!!! !!!!!!!!!!!!!!!!!!!!!!"""""""""""########&&&&&&&&&%%%%%%%%%$$$$$$$$#############"""""""""""""""!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"""""""""""############$''''&&&&&&&&&&%%%%%%%%%$$$$$$$$$##############"""""""""""""""""!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!""""""""""""""############$$$$$$''''''''''&&&&&&&&&&%%%%%%%%%$$$$$$$$$$################""""""""""""""""""""""""""""""""""""""""""""""""#############$$$$$$$$$$$$((((('''''''''''&&&&&&&&&&&%%%%%%%%%$$$$$$$$$$###################"""""""""""""""""""""""""""""#################$$$$$$$$$$$$$%%%%(((((((((((('''''''''''&&&&&&&&&&&%%%%%%%%%%$$$$$$$$$$$$##############################################$$$$$$$$$$$$$$$$%%%%%%%%%%))))))(((((((((((((''''''''''''&&&&&&&&&&%%%%%%%%%%%$$$$$$$$$$$$$$$###############$$$$###$$$$$$$$$$$$$$$$$$$$$$$%%%%%%%%%%%%%%%&*)))))))))))))(((((((((((((''''''''''''&&&&&&&&&&&%%%%%%%%%%%%%$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$%%%%%%%%%%%%%%%%%%%%&&&&&&&&**********)))))))))))))((((((((((((''''''''''''&&&&&&&&&&&&%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%&&&&&&&&&&&&&&+++++++************)))))))))))))(((((((((((('''''''''''''&&&&&&&&&&&&&&&&&&&&&&&&&&%%%%%%%%%%%%%%%&&&&&&&&&&&&&&&&&&&&&&&''''''',,,++++++++++++++***********)))))))))))))(((((((((((((''''''''''''''''&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&'''''''''''''''',,,,,,,,,,,,,,+++++++++++++***********)))))))))))))((((((((((((((('''''''''''''''''''''''''''''''''''''''''''''''''''''''(((((((----------,,,,,,,,,,,,,,,++++++++++++***********))))))))))))))((((((((((((((((('''''''''''''''''''''''''((((((((((((((((((((((((........---------------,,,,,,,,,,,,,++++++++++++************))))))))))))))))(((((((((((((((((((((((((((((((((((((((())))))))))))//////................-------------,,,,,,,,,,,,,++++++++++++**************)))))))))))))))))))))))))))))))))))))))))))))))))))***00///////////////////..............------------,,,,,,,,,,,,++++++++++++++*******************************************************000000000000000000000///////////////............-----------,,,,,,,,,,,,,++++++++++++++++++++++++*******************+++++++++++++1111111111111111111000000000000000000////////////............------------,,,,,,,,,,,,,,,,,++++++++++++++++++++++++++++++++++++++222222222222222222221111111111111111100000000000000///////////...........---------------,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,333333333333333333333222222222222222222111111111111100000000000///////////..............----------------------------------------4444444444444444444433333333333333333333332222222222221111111111100000000000////////////....................--------------------5555555555555555555555444444444444444444444433333333333332222222222111111111100000000000///////////////.........................66666666666666666666666655555555555555555555554444444444443333333333222222222111111111100000000000000///////////////////////////777777777777777777777777777777766666666666666666665555555555544444444433333333332222222221111111111110000000000000000000000/////888888888888888888888888888888888888888777777777777777666666666655555555544444444333333333222222222221111111111111111100000000009999999999999999999999999999999999999999999888888888888887777777777666666655555555544444444333333333322222222222222111111111111166666666666666666666666666655555555555555555555554444444444444433333333333333333222222222222222222222111111111111111111111111111000000/////////////............-------,,,,,,,,,,,,,++++++++++++++++++++++++++,,,,,,,,,,,,-------.................///////////////*))))))))((((((''''''&&&&&&%%%%%%$$$$$##""""""!!!!!! !!!!!"""""""##$$$$%%%%%%%&&&&&&'''''((((((()))))))**""""!!!!  !!!!!"""" !  !!!!!  """"!!!!!!  !!####"""""!!!!!  !!!!!$$$#####""""""!!!!!!  !!!!!!"""%%%$$$$######""""""!!!!!!!! !!!!!!!!""""""#&&&%%%%%$$$$$######"""""""!!!!!!!!!!!!!!!!!!!!!!!!!!""""""######''''&&&&&%%%%%$$$$$$#######""""""""""""""""""""""""""######$$$$$(((((''''''&&&&&%%%%%$$$$$$$########################$$$$$$$$%%%%)))))))((((('''''&&&&&&%%%%%%%$$$$$$$$$$$$$$$$$$$$$%%%%%%%%%%&&&++*******)))))((((((''''''&&&&&&&&&%%%%%%%%%%%%%%%&%&&&&&&&&&&'',,,,,++++++*******)))))))((((((''''''''''''''''&&'''''''''''''((.--------,,,,,,,++++++*******)))))))((((((((((((((((((((((((()))///////.......-------,,,,,,,++++++*********)*))))))))))))*******1000000000000/////////.....------,,,,,,,++++++++++++++++++++++++222222222222111111111100000///////.....--------,,,,,,,,,,,,,,,,,33334343333333333333222222221111100000//////..........----------444444444444444444444444333333222221111100000////////...........3333333333333333333322222222111110000000//////./.........--..---///./..........-.-----,,,,,,,,,,+++++++++*++*+++++++++++++++++++(((('''''''&&&&%%%%$$$%$$$$$#################$$$$$$%%%%%%%%%&&&&  !!!  #"""!!!  !!$$###"""!!! !!!"""&%%%$$$###""""!!!!!!!!!!"""""###('''&&&%%%%$$$#############$$$$%)))))(((''''&&&%%%%%%%%%%%%%%&&&+++++*****)))(((('''''''''''''''-----,,,,,,++++****)))))((((((((---.....------,,,++++****))))))),,,,,,,,,,,,,,++++***)))))((((((((((((((('''''&&&&&%%%%%%%%%%%%%##""""!!!!    ""!!  !!$$###""!!!!!!"""&&&%%%$$$#####$$'''''''&&%%%%%$$'''''''&&%%%$$$$$#####""""!!!!!!!  #"""!!!!####""!!  %%%%%%%$$$$$$$$$$$$$$$$$#######################"""""""""""""""!!!!!!!!!!!!!!!!!!!!! %%%%%%%%%$$$$$$$$$$$$$$$$$$######################"""""""""""""""!!!!!!!!!!!!!!!!!!!! %%%%%%%%%%%%$$$$$$$$$$$$$$$$$$#####################"""""""""""""""!!!!!!!!!!!!!!!!!!!! %%%%%%%%%%%%%%$$$$$$$$$$$$$$$$$$####################"""""""""""""""!!!!!!!!!!!!!!!!!!!! %%%%%%%%%%%%%%%%%$$$$$$$$$$$$$$$$$$###################"""""""""""""""!!!!!!!!!!!!!!!!!!! &%%%%%%%%%%%%%%%%%%%$$$$$$$$$$$$$$$$$$#################""""""""""""""""!!!!!!!!!!!!!!!!!!! &&&&%%%%%%%%%%%%%%%%%%%$$$$$$$$$$$$$$$$$#################""""""""""""""""!!!!!!!!!!!!!!!!!! &&&&&&&%%%%%%%%%%%%%%%%%%$$$$$$$$$$$$$$$$$$################""""""""""""""""!!!!!!!!!!!!!!!!! &&&&&&&&&&%%%%%%%%%%%%%%%%%%$$$$$$$$$$$$$$$$$################""""""""""""""""!!!!!!!!!!!!!!!!! &&&&&&&&&&&&&%%%%%%%%%%%%%%%%%$$$$$$$$$$$$$$$$$###############""""""""""""""""!!!!!!!!!!!!!!!!!!!!! &&&&&&&&&&&&&&&&%%%%%%%%%%%%%%%%$$$$$$$$$$$$$$$$$###############""""""""""""""""!!!!!!!!!!!!!!!!!!!!!!!! &&&&&&&&&&&&&&&&&&&%%%%%%%%%%%%%%%$$$$$$$$$$$$$$$$$###############""""""""""""""""!!!!!!!!!!!!!!!!!!!!!!!!!! '&&&&&&&&&&&&&&&&&&&&&&%%%%%%%%%%%%%$$$$$$$$$$$$$$$$$###############""""""""""""""""!!!!!!!!!!!!!!!!!!!!!!!!!!! ''''&&&&&&&&&&&&&&&&&&&&&&%%%%%%%%%%%%$$$$$$$$$$$$$$$$$##############""""""""""""""""!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ''''''''&&&&&&&&&&&&&&&&&&&&&%%%%%%%%%%%%$$$$$$$$$$$$$$$$##############"""""""""""""""""!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ''''''''''''&&&&&&&&&&&&&&&&&&&%%%%%%%%%%%%$$$$$$$$$$$$$$$$##############""""""""""""""""""""!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ''''''''''''''''&&&&&&&&&&&&&&&&&%%%%%%%%%%%%$$$$$$$$$$$$$$$$##############""""""""""""""""""""""!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!''''''''''''''''''''&&&&&&&&&&&&&&&&%%%%%%%%%%%$$$$$$$$$$$$$$$$#############""""""""""""""""""""""""""!!!!!!!!!!!!!!!!!!!!!!!!!!''''''''''''''''''''''''&&&&&&&&&&&&&&%%%%%%%%%%%%$$$$$$$$$$$$$$$##############""""""""""""""""""""""""""!!!!!!!!!!!!!!!!!!!!!!!((''''''''''''''''''''''''''&&&&&&&&&&&&%%%%%%%%%%%%%$$$$$$$$$$$$$$###############"""""""""""""""""""""""""""!!!!!!!!!!!!!!!!!!!((((((('''''''''''''''''''''''&&&&&&&&&&&&&%%%%%%%%%%%%$$$$$$$$$$$$$$###############""""""""""""""""""""""""""""!!!!!!!!!!!!!!!!(((((((((((('''''''''''''''''''''&&&&&&&&&&&&&%%%%%%%%%%%%$$$$$$$$$$$$$$#################"""""""""""""""""""""""""""!!!!!!!!!!!!((((((((((((((((''''''''''''''''''''&&&&&&&&&&&&%%%%%%%%%%%%%$$$$$$$$$$$$$$##################""""""""""""""""""""""""""!!!!!!!!!((((((((((((((((((((('''''''''''''''''&&&&&&&&&&&&&%%%%%%%%%%%%$$$$$$$$$$$$$$$####################""""""""""""""""""""""""""""""))))(((((((((((((((((((((('''''''''''''''&&&&&&&&&&&&&%%%%%%%%%%%%$$$$$$$$$$$$$$$####################"""""""""""""""""""""""""""))))))))((((((((((((((((((((((''''''''''''''&&&&&&&&&&&&&%%%%%%%%%%%%$$$$$$$$$$$$$$$####################""""""""""""""""""""""""))))))))))))(((((((((((((((((((((''''''''''''''&&&&&&&&&&&&&%%%%%%%%%%%%$$$$$$$$$$$$$$$####################"""""""""""""""""""""))))))))))))))))((((((((((((((((((((''''''''''''''&&&&&&&&&&&&&%%%%%%%%%%%%$$$$$$$$$$$$$$#######################""""""""""""""""*****)))))))))))))))(((((((((((((((((((''''''''''''''&&&&&&&&&&&&&%%%%%%%%%%%%$$$$$$$$$$$$$$####################################**********)))))))))))))))(((((((((((((((((('''''''''''''&&&&&&&&&&&&%%%%%%%%%%%%%$$$$$$$$$$$$$$$################################**************)))))))))))))))))((((((((((((((('''''''''''''&&&&&&&&&&&&%%%%%%%%%%%%%$$$$$$$$$$$$$$$#############################+******************)))))))))))))))((((((((((((((('''''''''''''&&&&&&&&&&&%%%%%%%%%%%%%$$$$$$$$$$$$$$$$$$$$$$####################+++++++*****************))))))))))))))(((((((((((((('''''''''''''&&&&&&&&&&&%%%%%%%%%%%%%$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$#########++++++++++++*****************)))))))))))))((((((((((((((''''''''''''&&&&&&&&&&&%%%%%%%%%%%%%%$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$++++++++++++++++++****************))))))))))))((((((((((((('''''''''''&&&&&&&&&&&%%%%%%%%%%%%%%%%%%$$$$$$$$$$$$$$$$$$$$$$$$$$$$$,,,,,,++++++++++++++++++**************))))))))))))(((((((((((('''''''''''&&&&&&&&&&&&&&%%%%%%%%%%%%%%%%%%%%%$$$$$$$$$$$$$$$$$$$$,,,,,,,,,,,,++++++++++++++++++*************)))))))))))(((((((((((''''''''''''&&&&&&&&&&&&&&&%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%--,,,,,,,,,,,,,,,++++++++++++++++++************)))))))))))(((((((((((''''''''''''&&&&&&&&&&&&&&&&%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%---------,,,,,,,,,,,,,,,++++++++++++++++************))))))))))((((((((((((''''''''''''&&&&&&&&&&&&&&&&%%%%%%%%%%%%%%%%%%%%%%%%%%----------------,,,,,,,,,,,,,,++++++++++++++************))))))))))(((((((((((('''''''''''''&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&.....-----------------,,,,,,,,,,,,,,+++++++++++++***********)))))))))))((((((((((('''''''''''''&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&.............----------------,,,,,,,,,,,,+++++++++++++**********)))))))))))(((((((((('''''''''''''''''''''&&&&&&&&&&&&&&&&&&&&&&....................---------------,,,,,,,,,,,++++++++++++**********))))))))))((((((((((((((''''''''''''''''''''''''''''''''''''/////////...................-------------,,,,,,,,,,+++++++++++**********)))))))))))))(((((((((((((((((''''''''''''''''''''''''''///////////////////................------------,,,,,,,,,++++++++++************)))))))))))))(((((((((((((((((((((((''''''''''''''0000///////////////////////..............-----------,,,,,,,,,,++++++++++************)))))))))))))))(((((((((((((((((((((((((((((0000000000000000///////////////////............-----------,,,,,,,,,,++++++++++***********))))))))))))))))))(((((((((((((((((((((11110000000000000000000000////////////////...........----------,,,,,,,,,,+++++++++++************))))))))))))))))))))))))))))))))11111111111111111100000000000000000/////////////..........----------,,,,,,,,,,+++++++++++****************)))))))))))))))))))))))222111111111111111111111111100000000000000////////////..........---------,,,,,,,,,,++++++++++++**************************)))))))2222222222222222222211111111111111111000000000000//////////..........---------,,,,,,,,,,,++++++++++++++++***********************33333322222222222222222222222222111111111111100000000000/////////.........----------,,,,,,,,,,,,+++++++++++++++++++++++++*******33333333333333333333333322222222222222222111111111111000000000/////////.........-----------,,,,,,,,,,,,,,,++++++++++++++++++++++44444444443333333333333333333333333322222222222221111111111000000000/////////.........-------------,,,,,,,,,,,,,,,,,,,,,,,,+++++44444444444444444444444444444333333333333333322222222222111111111000000000/////////..........---------------,,,,,,,,,,,,,,,,,,,,55555555555555555554444444444444444444444333333333333222222222211111111000000000/////////............--------------------------,55555555555555555555555555555555555544444444444444333333333322222222211111111000000000//////////...............-----------------66666666666666666666666666555555555555555555555444444444433333333322222222211111111000000000////////////........................666666666666666666666666666666666666666666555555555555544444444433333333222222221111111110000000000///////////////..............77777777777777777777777777777777777766666666666666665555555555444444443333333322222222111111111000000000000/////////////////////777777777777777888888777777777777777777777777777666666666665555555554444444433333333222222221111111111100000000000000000////////888888888888888888888888888888888888888888888777777777777666666666555555554444444433333333222222222211111111111110000000000000009999999999999999999999999999999999999999988888888888888777777777766666665555555554444444333333333222222222221111111111111111110099999999999999999::::::::::::::::99999999999999999999888888888877777777666666665555555544444444333333333322222222222222111111111666666666666666666666666666666666666665555555555555555544444444444443333333333333333222222222222222222221111111111111111111111110000000000////////////.............------,,,,,,,,,,,,,,++++++++++++++++++++,,,,,,,,,,,,,------...............///////////////////***))))))))(((((('''''&&&&&&&%%%%%%$$$##""""""""!!!!! !!!!!""""""""##$$$%%%%%%%&&&&&&'''''((((((())))))))**"""""!!!!  !!!!!""""%%%%%$$$$$$$$$##########""""""""!!!!!!!!!! %%%%%%$$$$$$$$$$##########""""""""!!!!!!!!! %%%%%%%%%$$$$$$$$$#########""""""""!!!!!!!!!! &&&%%%%%%%%%$$$$$$$$$########""""""""!!!!!!!!! &&&&&&%%%%%%%%$$$$$$$$$########""""""""!!!!!!!!!! &&&&&&&&&%%%%%%%%$$$$$$$$########""""""""!!!!!!!!!!!! ''&&&&&&&&&&&%%%%%%$$$$$$$$#######"""""""""!!!!!!!!!!!!!!! '''''&&&&&&&&&&%%%%%%$$$$$$$$$######""""""""""!!!!!!!!!!!!!!!! '''''''''&&&&&&&&%%%%%%%$$$$$$$########"""""""""""!!!!!!!!!!!!!!''''''''''''&&&&&&&&%%%%%%$$$$$$$########""""""""""""""!!!!!!!!!((((((''''''''''&&&&&&&%%%%%%$$$$$$$########""""""""""""""!!!!!!(((((((((((''''''''&&&&&&%%%%%%%$$$$$$##########"""""""""""""""!))))((((((((((''''''''&&&&&&%%%%%%$$$$$$$$#########""""""""""""")))))))((((((((((('''''''&&&&&&%%%%%%$$$$$$$$############"""""""****)))))))))((((((((''''''&&&&&&%%%%%%%$$$$$$$$################********)))))))))(((((((''''''&&&&&&%%%%%%%$$$$$$$$$############+++++*********))))))(((((((''''''&&&&&&%%%%%%%%$$$$$$$$$$$$$$$$$,,,+++++++********)))))))((((((''''''&&&&&&%%%%%%%%%%$$$$$$$$$$$,,,,,,,,++++++++*******)))))(((((('''''''&&&&&&&%%%%%%%%%%%%%%%%------,,,,,,,,+++++++******))))))((((('''''''&&&&&&&&&&&&&&&&&&&....---------,,,,,,,++++++*****))))))(((((''''''''''&&&&&&&&&&&&//...........------,,,,,,+++++******))))))((((((((''''''''''''''///////////........------,,,,,+++++******)))))))((((((((((((((((00000000000////////......-----,,,,,++++++******)))))))))))))((((11111111111000000000/////.....-----,,,,,++++++***********)))))))222222222222111111111000000////.....-----,,,,,+++++++++*********33333333333333222222221111100000/////....------,,,,,,,,,++++++++44444444444444443333333222222111110000////......-------,,,,,,,,,5555555555555554554444444433333222111110000//////......---------5666666666666666665555555544444333322222111100000//////.........6566666666666666666666655555554443333322221111000000////////....444444444444444444444343333332222221111100000////////...........////////////./.......-------,,,,,,,,++++++++++++++++++++++++++++)((((((('''&&&&&&%%%%%%$$$$$$#$$###$###$###$$$$$$$$%%%%%%%&%&&&&!!  %%%%$$$$$####"""""!!!!! &&%%%%%$$$$####""""!!!!!! &&&&&%%%%$$$$####""""!!!!!!!! '''&&&&%%%%$$$$####"""""!!!!!!!!('''''&&&&%%%%$$$####""""""""!!!(((((''''&&&&%%%$$$$#####"""""""))))((((('''&&&%%%%$$$$#########****))))(((('''&&&%%%%$$$$$$$$##+++++****)))((('''&&&&%%%%%%$$$$,,,,,,++++***)))((('''&&&&&&%%%%..-----,,,,+++***)))(((''''''''&////......---,,,++***))))(((((((0000000/////...--,,,++****))))))0111111100000///..---,,+++*****)000111111110000//..---,,+++*****....//////.....---,,+++***)))))(*****))))))(((((''''&&&&&&&&%%%%$$####""""!!!! !!&%%$$$##"""!!!!!'&&&%%$$##""""!!('''&&%%$$###""")))((''&&%%$$$##****))((''&&%%%$++++++**)(('&&&%+,,,,,++*))(''&&*******))(''&&%%&&&%%%%$$###""""! %%$$#""!''&%$$#"((('&%$#'''&&$#"##""! $$#"$$#! !  \ No newline at end of file diff --git a/TestCommon/Data/PlayerWithTypeTrees/sharedassets1.assets b/TestCommon/Data/PlayerWithTypeTrees/sharedassets1.assets new file mode 100644 index 0000000..b013984 Binary files /dev/null and b/TestCommon/Data/PlayerWithTypeTrees/sharedassets1.assets differ diff --git a/TextDumper/README.md b/TextDumper/README.md index 11b6b8e..8b801b4 100644 --- a/TextDumper/README.md +++ b/TextDumper/README.md @@ -1,60 +1,3 @@ # TextDumper -The TextDumper is a class library that can be used to dump the content of a Unity data -file (AssetBundle or SerializedFile) into human-readable yaml-style text file. - -## How to use - -The library consists of a single class called [TextDumperTool](./TextDumperTool.cs). It has a method named Dump and takes four 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. - -## How to interpret the output files - -There will be one output file per SerializedFile. Depending on the type of the input file, there can -be more than one output file (e.g. AssetBundles are archives that can contain several -SerializedFiles). - -The first lines of the output file looks like this: - - External References - path(1): "Library/unity default resources" GUID: 0000000000000000e000000000000000 Type: 0 - path(2): "Resources/unity_builtin_extra" GUID: 0000000000000000f000000000000000 Type: 0 - path(3): "archive:/CAB-35fce856128a6714740898681ea54bbe/CAB-35fce856128a6714740898681ea54bbe" GUID: 00000000000000000000000000000000 Type: 0 - -This information can be used to dereference PPtrs. A PPtr is a type used by Unity to locate and load -objects in SerializedFiles. It has two fields: -* m_FileID: The file identifier is an index in the External References list above (the number in parenthesis). It will be 0 if the asset is in the same file. -* m_PathID: The object identifier in the file. Each object in a file has a unique 64 identifier, often called a Local File Identifier (LFID). - -The string after the path is the SerializedFile name corresponding to the file identifier in -parenthesis. In the case of AssetBundles this can be the path of a file inside another AssetBundle (e.g. a path starting with "archive:". The GUID and Type are internal data used by Unity. - -The rest of the file will contain an entry similar to this one for each object in the files: - - ID: -8138362113332287275 (ClassID: 135) SphereCollider - m_GameObject PPtr - m_FileID int 0 - m_PathID SInt64 -1473921323670530447 - m_Material PPtr - m_FileID int 0 - m_PathID SInt64 0 - m_IsTrigger bool False - m_Enabled bool True - m_Radius float 0.5 - m_Center Vector3f - x float 0 - y float 0 - z float 0 - -The first line contains the object identifier, the internal ClassID used by Unity, and the type name -corresponding to this ClassID. Note that the object identifier is guaranteed to be unique in this -file only. - -The next lines are the serialized fields of the objects. The first value is the field -name, the second is the type and the last is the value. If there is no value, it means that it is a -sub-object that is dumped on the next lines with a higher indentation level. - -Note: This tool is similar to the binary2text.exe executable that is included with Unity. However the syntax of the output is somewhat different. \ No newline at end of file +See [Documentation/textdumper.md](../Documentation/textdumper.md) diff --git a/UnityDataTool.Tests/BuildReportTests.cs b/UnityDataTool.Tests/BuildReportTests.cs index 2e8833e..7378de4 100644 --- a/UnityDataTool.Tests/BuildReportTests.cs +++ b/UnityDataTool.Tests/BuildReportTests.cs @@ -18,7 +18,7 @@ public class BuildReportTests public void OneTimeSetup() { m_TestOutputFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "test_folder"); - m_TestDataFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data"); + m_TestDataFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "BuildReports"); Directory.CreateDirectory(m_TestOutputFolder); Directory.SetCurrentDirectory(m_TestOutputFolder); } @@ -44,12 +44,9 @@ public void Teardown() public async Task Analyze_BuildReport_ContainsExpected_ObjectInfo( [Values(false, true)] bool skipReferences) { - // This folder contains a reference build report generated by a build of the TestProject - // in the BuildReportInspector package. - var path = Path.Combine(m_TestDataFolder, "BuildReport1"); var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); - var args = new List { "analyze", path, "-p", "*.buildreport" }; + var args = new List { "analyze", m_TestDataFolder, "-p", "AssetBundle.buildreport" }; if (skipReferences) args.Add("--skip-references"); @@ -104,7 +101,7 @@ public async Task Analyze_BuildReport_ContainsExpected_ObjectInfo( SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(DISTINCT serialized_file) FROM object_view", 1, "All objects should be from the same serialized file"); - SQLTestHelper.AssertQueryString(db, "SELECT DISTINCT serialized_file FROM object_view", "LastBuild.buildreport", + SQLTestHelper.AssertQueryString(db, "SELECT DISTINCT serialized_file FROM object_view", "AssetBundle.buildreport", "Unexpected serialized file name in object_view"); // Verify the BuildReport object has expected properties @@ -139,7 +136,7 @@ public async Task Analyze_BuildReport_ContainsExpected_ObjectInfo( SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM serialized_files", 1, "Expected exactly one serialized file"); - SQLTestHelper.AssertQueryString(db, "SELECT name FROM serialized_files WHERE id = 0", "LastBuild.buildreport", + SQLTestHelper.AssertQueryString(db, "SELECT name FROM serialized_files WHERE id = 0", "AssetBundle.buildreport", "Unexpected serialized file name"); // Verify asset_bundle column is empty/NULL for BuildReport files (they are not asset bundles) @@ -160,10 +157,9 @@ public async Task Analyze_BuildReport_ContainsExpected_ObjectInfo( public async Task Analyze_BuildReport_ContainsExpectedReferences( [Values(false, true)] bool skipReferences) { - var path = Path.Combine(m_TestDataFolder, "BuildReport1"); var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); - var args = new List { "analyze", path, "-p", "*.buildreport" }; + var args = new List { "analyze", m_TestDataFolder, "-p", "AssetBundle.buildreport" }; if (skipReferences) args.Add("--skip-references"); @@ -212,4 +208,265 @@ public async Task Analyze_BuildReport_ContainsExpectedReferences( Assert.AreEqual(0, duplicateRefs, "No object should be referenced more than once"); } + + [Test] + public async Task Analyze_BuildReport_AssetBundle_ContainsBuildReportData() + { + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); + + var args = new List { "analyze", m_TestDataFolder, "-p", "AssetBundle.buildreport" }; + + Assert.AreEqual(0, await Program.Main(args.ToArray())); + using var db = SQLTestHelper.OpenDatabase(databasePath); + + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM build_reports", 1, + "Expected exactly one row in build_reports table"); + SQLTestHelper.AssertQueryString(db, "SELECT platform_name FROM build_reports", "Win64", + "Unexpected platform_name"); + SQLTestHelper.AssertQueryString(db, "SELECT build_type FROM build_reports", "AssetBundle", + "Unexpected build_type"); + SQLTestHelper.AssertQueryInt(db, "SELECT subtarget FROM build_reports", 2, + "Unexpected subtarget"); + SQLTestHelper.AssertQueryInt(db, "SELECT total_errors FROM build_reports", 0, + "Unexpected total_errors"); + SQLTestHelper.AssertQueryInt(db, "SELECT total_warnings FROM build_reports", 0, + "Unexpected total_warnings"); + SQLTestHelper.AssertQueryString(db, "SELECT build_result FROM build_reports", "Succeeded", + "Unexpected build_result"); + + var outputPath = SQLTestHelper.QueryString(db, "SELECT output_path FROM build_reports"); + Assert.That(outputPath, Does.Contain("AssetBundles"), "Output path should contain 'AssetBundles'"); + + var totalSize = SQLTestHelper.QueryInt(db, "SELECT total_size FROM build_reports"); + Assert.That(totalSize, Is.GreaterThan(0), "total_size should be greater than 0"); + } + + [Test] + public async Task Analyze_BuildReport_Player_ContainsBuildReportData() + { + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); + + var args = new List { "analyze", m_TestDataFolder, "-p", "Player.buildreport" }; + + Assert.AreEqual(0, await Program.Main(args.ToArray())); + using var db = SQLTestHelper.OpenDatabase(databasePath); + + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM build_reports", 1, + "Expected exactly one row in build_reports table"); + SQLTestHelper.AssertQueryString(db, "SELECT build_type FROM build_reports", "Player", + "Unexpected build_type"); + + // These checks are based on knowledge what the specific values in this test build report + SQLTestHelper.AssertQueryString(db, "SELECT build_guid FROM build_reports", "c743e3c6c0a541a69eae606c7991234e", + "Unexpected build_guid"); + SQLTestHelper.AssertQueryInt(db, "SELECT subtarget FROM build_reports", 2, + "Unexpected subtarget"); + SQLTestHelper.AssertQueryInt(db, "SELECT options FROM build_reports", 137, + "Unexpected options"); + SQLTestHelper.AssertQueryString(db, "SELECT build_result FROM build_reports", "Succeeded", + "Unexpected build_result"); + SQLTestHelper.AssertQueryString(db, "SELECT start_time FROM build_reports", "2025-12-29T13:03:00.5010432Z", + "Unexpected start time"); + SQLTestHelper.AssertQueryString(db, "SELECT end_time FROM build_reports", "2025-12-29T13:03:06.3987171Z", + "Unexpected end time"); + SQLTestHelper.AssertQueryInt(db, "SELECT total_time_seconds FROM build_reports", 6, + "Unexpected total_time_seconds"); + + var totalSize = SQLTestHelper.QueryInt(db, "SELECT total_size FROM build_reports"); + Assert.That(totalSize, Is.GreaterThan(0), "total_size should be greater than 0"); + + var outputPath = SQLTestHelper.QueryString(db, "SELECT output_path FROM build_reports"); + Assert.That(outputPath, Does.Contain("TestProject.exe"), "Output path should contain 'TestProject.exe'"); + } + + [Test] + public async Task Analyze_BuildReport_AssetBundle_ContainsPackedAssetsData() + { + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); + + var args = new List { "analyze", m_TestDataFolder, "-p", "AssetBundle.buildreport" }; + + Assert.AreEqual(0, await Program.Main(args.ToArray())); + using var db = SQLTestHelper.OpenDatabase(databasePath); + + // Verify the build_report_packed_assets table has the expected number of rows + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM build_report_packed_assets", 7, + "Expected exactly 7 rows in build_report_packed_assets table"); + + // Verify the specific PackedAssets object (corresponds to raw object ID -2699881322159949766 in the file) + const string path = "CAB-6b49068aebcf9d3b05692c8efd933167"; + SQLTestHelper.AssertQueryInt(db, $"SELECT COUNT(*) FROM build_report_packed_assets WHERE path = '{path}'", 1, + $"Expected exactly one PackedAssets with path = {path}"); + + SQLTestHelper.AssertQueryInt(db, $"SELECT file_header_size FROM build_report_packed_assets WHERE path = '{path}'", 10720, + "Unexpected file_header_size for PackedAssets"); + + // Get the database ID for this PackedAssets + var packedAssetId = SQLTestHelper.QueryInt(db, $"SELECT id FROM build_report_packed_assets WHERE path = '{path}'"); + + // Verify there are 7 content rows for this PackedAssets + SQLTestHelper.AssertQueryInt(db, $"SELECT COUNT(*) FROM build_report_packed_asset_info WHERE packed_assets_id = {packedAssetId}", 7, + "Expected exactly 7 rows in build_report_packed_asset_info for this PackedAssets"); + + // Verify the specific content row (data[3] from the dump) + const long objectId = -1350043613627603771; + var contentRow = SQLTestHelper.QueryInt(db, + $@"SELECT COUNT(*) FROM build_report_packed_asset_contents_view + WHERE packed_assets_id = {packedAssetId} + AND object_id = {objectId} + AND type = 28 + AND size = 204 + AND offset = 11840 + AND source_asset_guid = '8826f464101b93c4bb006e15a9aff317' + AND build_time_asset_path = 'Assets/Sprites/Snow.jpg'"); + + Assert.AreEqual(1, contentRow, + "Expected exactly one packed_asset_contents row matching the specified criteria"); + + // Verify the view works correctly for this content row + SQLTestHelper.AssertQueryString(db, + $@"SELECT source_asset_guid FROM build_report_packed_asset_contents_view + WHERE packed_assets_id = {packedAssetId} + AND object_id = {objectId}", + "8826f464101b93c4bb006e15a9aff317", + "Unexpected source_asset_guid in build_report_packed_asset_contents_view"); + + SQLTestHelper.AssertQueryString(db, + $@"SELECT build_time_asset_path FROM build_report_packed_asset_contents_view + WHERE packed_assets_id = {packedAssetId} + AND object_id = {objectId}", + "Assets/Sprites/Snow.jpg", + "Unexpected build_time_asset_path in build_report_packed_asset_contents_view"); + + SQLTestHelper.AssertQueryString(db, + $"SELECT path FROM build_report_packed_assets_view WHERE id = {packedAssetId}", + "CAB-6b49068aebcf9d3b05692c8efd933167", + "Unexpected path in build_report_packed_assets_view"); + } + + [Test] + public async Task Analyze_BuildReports_BothReports_ContainsBuildReportFilesData() + { + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); + + // Analyze multiple BuildReports into the same database + var args = new List { "analyze", m_TestDataFolder, "-p", "*.buildreport" }; + + Assert.AreEqual(0, await Program.Main(args.ToArray())); + using var db = SQLTestHelper.OpenDatabase(databasePath); + + // Verify we have 2 BuildReports + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM build_reports", 2, + "Expected exactly 2 BuildReports"); + + // Verify we have files from both BuildReports + var totalFiles = SQLTestHelper.QueryInt(db, "SELECT COUNT(*) FROM build_report_files"); + Assert.That(totalFiles, Is.GreaterThan(0), "Expected at least some files in build_report_files"); + + // Verify that an expected file from AssetBundle.buildreport is present + var assetBundleFileCount = SQLTestHelper.QueryInt(db, + @"SELECT COUNT(*) FROM build_report_files + WHERE path = 'audio.bundle/CAB-76a378bdc9304bd3c3a82de8dd97981a.resource'"); + Assert.AreEqual(1, assetBundleFileCount, + "Expected to find one file with 'CAB-76a378bdc9304bd3c3a82de8dd97981a.resource' in path from AssetBundle.buildreport"); + + // Verify that an expected file from Player.buildreport is present + var playerFileCount = SQLTestHelper.QueryInt(db, + @"SELECT COUNT(*) FROM build_report_files + WHERE path = 'TestProject_Data/sharedassets0.assets.resS'"); + Assert.AreEqual(1, playerFileCount, + "Expected to find one file with 'sharedassets0.assets.resS' in path from Player.buildreport"); + + // Verify that each BuildReport has its own set of files with the correct build_report_id + var assetBundleReportId = SQLTestHelper.QueryInt(db, + "SELECT id FROM build_reports WHERE build_type = 'AssetBundle'"); + var playerReportId = SQLTestHelper.QueryInt(db, + "SELECT id FROM build_reports WHERE build_type = 'Player'"); + + var assetBundleFileCountByReportId = SQLTestHelper.QueryInt(db, + $"SELECT COUNT(*) FROM build_report_files WHERE build_report_id = {assetBundleReportId}"); + Assert.That(assetBundleFileCountByReportId, Is.GreaterThan(0), + "Expected AssetBundle BuildReport to have files"); + + var playerFileCountByReportId = SQLTestHelper.QueryInt(db, + $"SELECT COUNT(*) FROM build_report_files WHERE build_report_id = {playerReportId}"); + Assert.That(playerFileCountByReportId, Is.GreaterThan(0), + "Expected Player BuildReport to have files"); + + // Verify the view includes serialized_file and can filter by it + var playerFilesInView = SQLTestHelper.QueryInt(db, + @"SELECT COUNT(*) FROM build_report_files_view + WHERE serialized_file = 'Player.buildreport'"); + Assert.That(playerFilesInView, Is.GreaterThan(0), + "Expected to find files from Player.buildreport in the view using serialized_file"); + + // Verify we can find the specific Player.buildreport file in the view + var specificPlayerFile = SQLTestHelper.QueryInt(db, + @"SELECT COUNT(*) FROM build_report_files_view + WHERE serialized_file = 'Player.buildreport' + AND path = 'TestProject_Data/sharedassets0.assets.resS'"); + Assert.AreEqual(1, specificPlayerFile, + "Expected to find exactly one row with path='TestProject_Data/sharedassets0.assets.resS' from Player.buildreport in view"); + + // Verify the serialized_file column correctly identifies the source BuildReport + var assetBundleSerializedFile = SQLTestHelper.QueryString(db, + @"SELECT DISTINCT serialized_file FROM build_report_files_view + WHERE path = 'audio.bundle/CAB-76a378bdc9304bd3c3a82de8dd97981a.resource'"); + Assert.AreEqual("AssetBundle.buildreport", assetBundleSerializedFile, + "Expected serialized_file to be 'AssetBundle.buildreport' for AssetBundle files"); + + var playerSerializedFile = SQLTestHelper.QueryString(db, + @"SELECT DISTINCT serialized_file FROM build_report_files_view + WHERE path = 'TestProject_Data/sharedassets0.assets.resS'"); + Assert.AreEqual("Player.buildreport", playerSerializedFile, + "Expected serialized_file to be 'Player.buildreport' for Player files"); + + // Verify build_report_archive_contents table has entries for AssetBundle build + var archiveContentsCount = SQLTestHelper.QueryInt(db, + $"SELECT COUNT(*) FROM build_report_archive_contents WHERE build_report_id = {assetBundleReportId}"); + Assert.That(archiveContentsCount, Is.GreaterThan(0), + "Expected AssetBundle BuildReport to have archive contents mappings"); + + // Verify specific archive content mapping exists + var spritesArchiveContentCount = SQLTestHelper.QueryInt(db, + $@"SELECT COUNT(*) FROM build_report_archive_contents + WHERE build_report_id = {assetBundleReportId} + AND assetbundle = 'sprites.bundle' + AND assetbundle_content = 'CAB-6b49068aebcf9d3b05692c8efd933167.resS'"); + Assert.AreEqual(1, spritesArchiveContentCount, + "Expected to find mapping for sprites.bundle -> CAB-6b49068aebcf9d3b05692c8efd933167.resS"); + + // Verify Player build has no archive contents (not an AssetBundle build) + var playerArchiveContentsCount = SQLTestHelper.QueryInt(db, + $"SELECT COUNT(*) FROM build_report_archive_contents WHERE build_report_id = {playerReportId}"); + Assert.AreEqual(0, playerArchiveContentsCount, + "Expected Player BuildReport to have no archive contents mappings"); + + // Verify build_report_packed_assets_view includes assetbundle column + var packedAssetsWithBundle = SQLTestHelper.QueryInt(db, + @"SELECT COUNT(*) FROM build_report_packed_assets_view + WHERE assetbundle IS NOT NULL"); + Assert.That(packedAssetsWithBundle, Is.GreaterThan(0), + "Expected some PackedAssets to have assetbundle name populated"); + + // Verify specific PackedAsset has correct assetbundle name + var specificPackedAssetBundle = SQLTestHelper.QueryString(db, + @"SELECT assetbundle FROM build_report_packed_assets_view + WHERE path = 'CAB-6b49068aebcf9d3b05692c8efd933167'"); + Assert.AreEqual("sprites.bundle", specificPackedAssetBundle, + "Expected PackedAsset CAB-6b49068aebcf9d3b05692c8efd933167 to have assetbundle 'sprites.bundle'"); + + // Verify PackedAssets from Player build have NULL assetbundle + var playerPackedAssetsWithNullBundle = SQLTestHelper.QueryInt(db, + @"SELECT COUNT(*) FROM build_report_packed_assets_view + WHERE build_report_filename = 'Player.buildreport' AND assetbundle IS NULL"); + Assert.That(playerPackedAssetsWithNullBundle, Is.GreaterThan(0), + "Expected PackedAssets from Player.buildreport to have NULL assetbundle"); + + var playerPackedAssetsWithNonNullBundle = SQLTestHelper.QueryInt(db, + @"SELECT COUNT(*) FROM build_report_packed_assets_view + WHERE build_report_filename = 'Player.buildreport' AND assetbundle IS NOT NULL"); + Assert.AreEqual(0, playerPackedAssetsWithNonNullBundle, + "Expected all PackedAssets from Player.buildreport have NULL assetbundle"); + } } diff --git a/UnityDataTool.Tests/ExpectedDataGenerator.cs b/UnityDataTool.Tests/ExpectedDataGenerator.cs index b127a3e..77707c2 100644 --- a/UnityDataTool.Tests/ExpectedDataGenerator.cs +++ b/UnityDataTool.Tests/ExpectedDataGenerator.cs @@ -36,12 +36,13 @@ public static void Generate(Context context) using (var cmd = db.CreateCommand()) { cmd.CommandText = - @"SELECT + @"SELECT (SELECT COUNT(*) FROM animation_clips), (SELECT COUNT(*) FROM asset_bundles), (SELECT COUNT(*) FROM assets), (SELECT COUNT(*) FROM audio_clips), (SELECT COUNT(*) FROM meshes), + (SELECT COUNT(*) FROM monoscripts), (SELECT COUNT(*) FROM objects), (SELECT COUNT(*) FROM refs), (SELECT COUNT(*) FROM serialized_files), @@ -61,15 +62,16 @@ public static void Generate(Context context) expectedData.Add("assets_count", reader.GetInt32(2)); expectedData.Add("audio_clips_count", reader.GetInt32(3)); expectedData.Add("meshes_count", reader.GetInt32(4)); - expectedData.Add("objects_count", reader.GetInt32(5)); - expectedData.Add("refs_count", reader.GetInt32(6)); - expectedData.Add("serialized_files_count", reader.GetInt32(7)); - expectedData.Add("shader_subprograms_count", reader.GetInt32(8)); - expectedData.Add("shaders_count", reader.GetInt32(9)); - expectedData.Add("shader_keywords_count", reader.GetInt32(10)); - expectedData.Add("shader_subprogram_keywords_count", reader.GetInt32(11)); - expectedData.Add("textures_count", reader.GetInt32(12)); - expectedData.Add("types_count", reader.GetInt32(13)); + expectedData.Add("monoscripts_count", reader.GetInt32(5)); + expectedData.Add("objects_count", reader.GetInt32(6)); + expectedData.Add("refs_count", reader.GetInt32(7)); + expectedData.Add("serialized_files_count", reader.GetInt32(8)); + expectedData.Add("shader_subprograms_count", reader.GetInt32(9)); + expectedData.Add("shaders_count", reader.GetInt32(10)); + expectedData.Add("shader_keywords_count", reader.GetInt32(11)); + expectedData.Add("shader_subprogram_keywords_count", reader.GetInt32(12)); + expectedData.Add("textures_count", reader.GetInt32(13)); + expectedData.Add("types_count", reader.GetInt32(14)); } var csprojFolder = Directory.GetParent(context.TestDataFolder).Parent.Parent.Parent.FullName; diff --git a/UnityDataTool.Tests/SQLTestHelper.cs b/UnityDataTool.Tests/SQLTestHelper.cs index d26f3dc..3f1c713 100644 --- a/UnityDataTool.Tests/SQLTestHelper.cs +++ b/UnityDataTool.Tests/SQLTestHelper.cs @@ -106,4 +106,30 @@ public static void AssertQueryString(SqliteConnection db, string sql, string exp reader.Read(); Assert.AreEqual(expectedValue, reader.GetString(0), description); } + + /// + /// Asserts that a table exists in the database. + /// + /// The database connection to use. + /// The name of the table to check for. + public static void AssertTableExists(SqliteConnection db, string tableName) + { + using var cmd = db.CreateCommand(); + cmd.CommandText = $"SELECT name FROM sqlite_master WHERE type='table' AND name='{tableName}'"; + using var reader = cmd.ExecuteReader(); + Assert.IsTrue(reader.Read(), $"{tableName} table should exist"); + } + + /// + /// Asserts that a view exists in the database. + /// + /// The database connection to use. + /// The name of the view to check for. + public static void AssertViewExists(SqliteConnection db, string viewName) + { + using var cmd = db.CreateCommand(); + cmd.CommandText = $"SELECT name FROM sqlite_master WHERE type='view' AND name='{viewName}'"; + using var reader = cmd.ExecuteReader(); + Assert.IsTrue(reader.Read(), $"{viewName} view should exist"); + } } diff --git a/UnityDataTool.Tests/SerializedFileCommandTests.cs b/UnityDataTool.Tests/SerializedFileCommandTests.cs new file mode 100644 index 0000000..c2b9591 --- /dev/null +++ b/UnityDataTool.Tests/SerializedFileCommandTests.cs @@ -0,0 +1,500 @@ +using System; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Data.Sqlite; +using NUnit.Framework; +using UnityDataTools.FileSystem; + +namespace UnityDataTools.UnityDataTool.Tests; + +#pragma warning disable NUnit2005, NUnit2006 + +/// +/// Tests for the serialized-file command using PlayerWithTypeTrees test data. +/// This data contains Player build output with TypeTrees enabled. +/// +public class SerializedFileCommandTests +{ + private string m_TestOutputFolder; + private string m_TestDataFolder; + + [OneTimeSetUp] + public void OneTimeSetup() + { + UnityFileSystem.Init(); + m_TestOutputFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "test_folder"); + m_TestDataFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "PlayerWithTypeTrees"); + Directory.CreateDirectory(m_TestOutputFolder); + Directory.SetCurrentDirectory(m_TestOutputFolder); + } + + [TearDown] + public void Teardown() + { + SqliteConnection.ClearAllPools(); + + var testDir = new DirectoryInfo(m_TestOutputFolder); + testDir.EnumerateFiles() + .ToList().ForEach(f => f.Delete()); + testDir.EnumerateDirectories() + .ToList().ForEach(d => d.Delete(true)); + } + + [OneTimeTearDown] + public void OneTimeTeardown() + { + UnityFileSystem.Cleanup(); + } + + #region ExternalRefs Tests + + [Test] + public async Task ExternalRefs_TextFormat_OutputsCorrectly() + { + var path = Path.Combine(m_TestDataFolder, "sharedassets0.assets"); + using var sw = new StringWriter(); + var currentOut = Console.Out; + try + { + Console.SetOut(sw); + + Assert.AreEqual(0, await Program.Main(new string[] { "serialized-file", "externalrefs", path })); + + var output = sw.ToString(); + var lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + + // sharedassets0.assets should have external references + Assert.Greater(lines.Length, 0, "Expected at least one external reference"); + + // Check format: "Index: N, Path: " + foreach (var line in lines) + { + StringAssert.Contains("Index:", line); + StringAssert.Contains("Path:", line); + } + } + finally + { + Console.SetOut(currentOut); + } + } + + [Test] + public async Task ExternalRefs_JsonFormat_OutputsValidJson() + { + var path = Path.Combine(m_TestDataFolder, "sharedassets0.assets"); + using var sw = new StringWriter(); + var currentOut = Console.Out; + try + { + Console.SetOut(sw); + + Assert.AreEqual(0, await Program.Main(new string[] { "serialized-file", "externalrefs", path, "-f", "json" })); + + var output = sw.ToString(); + + // Parse JSON to verify it's valid + var jsonArray = JsonDocument.Parse(output).RootElement; + Assert.IsTrue(jsonArray.ValueKind == JsonValueKind.Array); + + // Verify structure of each element + foreach (var element in jsonArray.EnumerateArray()) + { + Assert.IsTrue(element.TryGetProperty("index", out _)); + Assert.IsTrue(element.TryGetProperty("path", out _)); + Assert.IsTrue(element.TryGetProperty("guid", out _)); + Assert.IsTrue(element.TryGetProperty("type", out _)); + } + } + finally + { + Console.SetOut(currentOut); + } + } + + [Test] + public async Task ExternalRefs_Level0_HasExpectedReferences() + { + var path = Path.Combine(m_TestDataFolder, "level0"); + using var sw = new StringWriter(); + var currentOut = Console.Out; + try + { + Console.SetOut(sw); + + Assert.AreEqual(0, await Program.Main(new string[] { "serialized-file", "externalrefs", path, "-f", "json" })); + + var output = sw.ToString(); + var jsonArray = JsonDocument.Parse(output).RootElement; + + // level0 should reference sharedassets0.assets + bool foundSharedAssets = false; + foreach (var element in jsonArray.EnumerateArray()) + { + var pathValue = element.GetProperty("path").GetString(); + if (pathValue != null && pathValue.Contains("sharedassets0")) + { + foundSharedAssets = true; + break; + } + } + + Assert.IsTrue(foundSharedAssets, "Expected level0 to reference sharedassets0.assets"); + } + finally + { + Console.SetOut(currentOut); + } + } + + #endregion + + #region ObjectList Tests + + [Test] + public async Task ObjectList_TextFormat_OutputsTable() + { + var path = Path.Combine(m_TestDataFolder, "level0"); + using var sw = new StringWriter(); + var currentOut = Console.Out; + try + { + Console.SetOut(sw); + + Assert.AreEqual(0, await Program.Main(new string[] { "serialized-file", "objectlist", path })); + + var output = sw.ToString(); + var lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + + // Should have header line + Assert.Greater(lines.Length, 2, "Expected header and at least one data row"); + StringAssert.Contains("Id", lines[0]); + StringAssert.Contains("Type", lines[0]); + StringAssert.Contains("Offset", lines[0]); + StringAssert.Contains("Size", lines[0]); + + // Second line should be separator + StringAssert.Contains("---", lines[1]); + + // Should have data rows with numeric values + Assert.Greater(lines.Length, 2); + } + finally + { + Console.SetOut(currentOut); + } + } + + [Test] + public async Task ObjectList_JsonFormat_OutputsValidJson() + { + var path = Path.Combine(m_TestDataFolder, "level0"); + using var sw = new StringWriter(); + var currentOut = Console.Out; + try + { + Console.SetOut(sw); + + Assert.AreEqual(0, await Program.Main(new string[] { "serialized-file", "objectlist", path, "--format", "json" })); + + var output = sw.ToString(); + + // Parse JSON to verify it's valid + var jsonArray = JsonDocument.Parse(output).RootElement; + Assert.IsTrue(jsonArray.ValueKind == JsonValueKind.Array); + Assert.Greater(jsonArray.GetArrayLength(), 0); + + // Verify structure of each element + foreach (var element in jsonArray.EnumerateArray()) + { + Assert.IsTrue(element.TryGetProperty("id", out _)); + Assert.IsTrue(element.TryGetProperty("typeId", out _)); + Assert.IsTrue(element.TryGetProperty("typeName", out _)); + Assert.IsTrue(element.TryGetProperty("offset", out _)); + Assert.IsTrue(element.TryGetProperty("size", out _)); + } + } + finally + { + Console.SetOut(currentOut); + } + } + + [Test] + public async Task ObjectList_ShowsTypeNames_NotJustNumbers() + { + var path = Path.Combine(m_TestDataFolder, "level0"); + using var sw = new StringWriter(); + var currentOut = Console.Out; + try + { + Console.SetOut(sw); + + Assert.AreEqual(0, await Program.Main(new string[] { "sf", "objectlist", path, "-f", "json" })); + + var output = sw.ToString(); + var jsonArray = JsonDocument.Parse(output).RootElement; + + // Look for common Unity types by name (not just numeric TypeIds) + bool foundGameObject = false; + bool foundTransform = false; + + foreach (var element in jsonArray.EnumerateArray()) + { + var typeName = element.GetProperty("typeName").GetString(); + if (typeName == "GameObject") foundGameObject = true; + if (typeName == "Transform") foundTransform = true; + } + + Assert.IsTrue(foundGameObject, "Expected to find GameObject type"); + Assert.IsTrue(foundTransform, "Expected to find Transform type"); + } + finally + { + Console.SetOut(currentOut); + } + } + + [Test] + public async Task ObjectList_SharedAssets_ContainsExpectedTypes() + { + var path = Path.Combine(m_TestDataFolder, "sharedassets0.assets"); + using var sw = new StringWriter(); + var currentOut = Console.Out; + try + { + Console.SetOut(sw); + + Assert.AreEqual(0, await Program.Main(new string[] { "serialized-file", "objectlist", path, "-f", "json" })); + + var output = sw.ToString(); + var jsonArray = JsonDocument.Parse(output).RootElement; + + // SharedAssets should contain MonoBehaviour (114) or MonoScript (115) + bool foundScriptType = false; + + foreach (var element in jsonArray.EnumerateArray()) + { + var typeName = element.GetProperty("typeName").GetString(); + if (typeName == "MonoBehaviour" || typeName == "MonoScript") + { + foundScriptType = true; + break; + } + } + + Assert.IsTrue(foundScriptType, "Expected to find MonoBehaviour or MonoScript in sharedassets"); + } + finally + { + Console.SetOut(currentOut); + } + } + + #endregion + + #region Cross-Validation with Analyze Command + + [Test] + public async Task ObjectList_CrossValidate_MatchesAnalyzeCommand() + { + // First, run analyze command to create database + var databasePath = Path.Combine(m_TestOutputFolder, "test_analyze.db"); + var analyzePath = m_TestDataFolder; + Assert.AreEqual(0, await Program.Main(new string[] { "analyze", analyzePath, "-o", databasePath, "-p", "level0" })); + + // Now run serialized-file objectlist + var path = Path.Combine(m_TestDataFolder, "level0"); + using var sw = new StringWriter(); + var currentOut = Console.Out; + try + { + Console.SetOut(sw); + + Assert.AreEqual(0, await Program.Main(new string[] { "serialized-file", "objectlist", path, "-f", "json" })); + + var output = sw.ToString(); + var jsonArray = JsonDocument.Parse(output).RootElement; + var sfObjectCount = jsonArray.GetArrayLength(); + + // Query database for the same file + using var db = new SqliteConnection($"Data Source={databasePath}"); + db.Open(); + using var cmd = db.CreateCommand(); + cmd.CommandText = @" + SELECT COUNT(*) + FROM objects o + INNER JOIN serialized_files sf ON o.serialized_file = sf.id + WHERE sf.name = 'level0'"; + + var dbObjectCount = Convert.ToInt32(cmd.ExecuteScalar()); + + // Object counts should match + Assert.AreEqual(dbObjectCount, sfObjectCount, "Object count from serialized-file command should match analyze database"); + + // Verify a few specific objects match by type and size + cmd.CommandText = @" + SELECT o.object_id, t.name, o.size + FROM objects o + INNER JOIN types t ON o.type = t.id + INNER JOIN serialized_files sf ON o.serialized_file = sf.id + WHERE sf.name = 'level0' + LIMIT 5"; + + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + var dbObjectId = reader.GetInt64(0); + var dbTypeName = reader.GetString(1); + var dbSize = reader.GetInt64(2); + + // Find matching object in serialized-file output + bool found = false; + foreach (var element in jsonArray.EnumerateArray()) + { + var sfObjectId = element.GetProperty("id").GetInt64(); + if (sfObjectId == dbObjectId) + { + var sfTypeName = element.GetProperty("typeName").GetString(); + var sfSize = element.GetProperty("size").GetInt64(); + + Assert.AreEqual(dbTypeName, sfTypeName, $"Type name mismatch for object {dbObjectId}"); + Assert.AreEqual(dbSize, sfSize, $"Size mismatch for object {dbObjectId}"); + found = true; + break; + } + } + + Assert.IsTrue(found, $"Object {dbObjectId} found in database but not in serialized-file output"); + } + } + finally + { + Console.SetOut(currentOut); + } + } + + #endregion + + #region Format Option Tests + + [Test] + public async Task FormatOption_DefaultIsText() + { + var path = Path.Combine(m_TestDataFolder, "level0"); + using var sw = new StringWriter(); + var currentOut = Console.Out; + try + { + Console.SetOut(sw); + + Assert.AreEqual(0, await Program.Main(new string[] { "serialized-file", "objectlist", path })); + + var output = sw.ToString(); + + // Text format should have header line with "Id", "Type", etc. + StringAssert.Contains("Id", output); + StringAssert.Contains("Type", output); + StringAssert.Contains("Offset", output); + StringAssert.Contains("Size", output); + + // Should not start with '[' or '{' (not JSON) + Assert.IsFalse(output.TrimStart().StartsWith("[")); + Assert.IsFalse(output.TrimStart().StartsWith("{")); + } + finally + { + Console.SetOut(currentOut); + } + } + + [Test] + public async Task FormatOption_ShortAndLongForms_Work() + { + var path = Path.Combine(m_TestDataFolder, "level0"); + + // Test short form -f + using (var sw = new StringWriter()) + { + var currentOut = Console.Out; + try + { + Console.SetOut(sw); + Assert.AreEqual(0, await Program.Main(new string[] { "serialized-file", "objectlist", path, "-f", "json" })); + var output = sw.ToString(); + Assert.DoesNotThrow(() => JsonDocument.Parse(output)); + } + finally + { + Console.SetOut(currentOut); + } + } + + // Test long form --format + using (var sw = new StringWriter()) + { + var currentOut = Console.Out; + try + { + Console.SetOut(sw); + Assert.AreEqual(0, await Program.Main(new string[] { "serialized-file", "objectlist", path, "--format", "json" })); + var output = sw.ToString(); + Assert.DoesNotThrow(() => JsonDocument.Parse(output)); + } + finally + { + Console.SetOut(currentOut); + } + } + } + + [Test] + public async Task Alias_SF_Works() + { + var path = Path.Combine(m_TestDataFolder, "level0"); + using var sw = new StringWriter(); + var currentOut = Console.Out; + try + { + Console.SetOut(sw); + + // Use 'sf' alias instead of 'serialized-file' + Assert.AreEqual(0, await Program.Main(new string[] { "sf", "objectlist", path })); + + var output = sw.ToString(); + Assert.IsNotEmpty(output); + } + finally + { + Console.SetOut(currentOut); + } + } + + #endregion + + #region Error Handling Tests + + [Test] + public async Task ErrorHandling_InvalidFile_ReturnsError() + { + var path = Path.Combine(m_TestDataFolder, "README.md"); // Text file, not a SerializedFile + + var result = await Program.Main(new string[] { "serialized-file", "objectlist", path }); + Assert.AreNotEqual(0, result, "Should return error code for invalid file"); + } + + [Test] + public async Task ErrorHandling_NonExistentFile_ReturnsError() + { + var path = Path.Combine(m_TestDataFolder, "nonexistent.file"); + + // System.CommandLine should catch this and return error + var result = await Program.Main(new string[] { "serialized-file", "objectlist", path }); + Assert.AreNotEqual(0, result, "Should return error code for non-existent file"); + } + + #endregion +} + diff --git a/UnityDataTool.Tests/UnityDataToolAssetBundleTests.cs b/UnityDataTool.Tests/UnityDataToolAssetBundleTests.cs index 7a6e2ad..b47ae73 100644 --- a/UnityDataTool.Tests/UnityDataToolAssetBundleTests.cs +++ b/UnityDataTool.Tests/UnityDataToolAssetBundleTests.cs @@ -210,6 +210,40 @@ public async Task Analyze_WithOutputFile_DatabaseCorrect( ValidateDatabase(databasePath, true); } + [Test] + public async Task Analyze_MonoScripts_DatabaseContainsExpectedContent() + { + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); + var analyzePath = Path.Combine(Context.UnityDataFolder); + + Assert.AreEqual(0, await Program.Main(new string[] { "analyze", analyzePath })); + + using var db = SQLTestHelper.OpenDatabase(databasePath); + + // Verify MonoScript table and views exist + SQLTestHelper.AssertTableExists(db, "monoscripts"); + SQLTestHelper.AssertViewExists(db, "monoscript_view"); + SQLTestHelper.AssertViewExists(db, "script_object_view"); + + // Verify MonoScript table contains data + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM monoscripts", + 1, + "Unexpected number of MonoScripts"); + + // Verify the specific MonoScript from the example + // Note: Assembly name format changed in Unity 2023.1 from 'Assembly-CSharp.dll' to 'Assembly-CSharp' + SQLTestHelper.AssertQueryInt(db, + "SELECT COUNT(*) FROM monoscript_view WHERE class_name = 'SerializeReferencePolymorphismExample' AND assembly_name LIKE 'Assembly-CSharp%'", + 1, + "Expected to find SerializeReferencePolymorphismExample MonoScript"); + + // Verify script_object_view finds the SerializeReferencePolymorphismExample MonoBehaviour + SQLTestHelper.AssertQueryInt(db, + "SELECT COUNT(*) FROM script_object_view WHERE class_name = 'SerializeReferencePolymorphismExample'", + 1, + "Expected to find exactly one MonoBehaviour instance of SerializeReferencePolymorphismExample"); + } + private void ValidateDatabase(string databasePath, bool withRefs) { using var db = SQLTestHelper.OpenDatabase(databasePath); diff --git a/UnityDataTool/Program.cs b/UnityDataTool/Program.cs index bf95060..59fae2d 100644 --- a/UnityDataTool/Program.cs +++ b/UnityDataTool/Program.cs @@ -9,6 +9,12 @@ namespace UnityDataTools.UnityDataTool; +public enum OutputFormat +{ + Text, + Json +} + public static class Program { public static async Task Main(string[] args) @@ -124,6 +130,41 @@ public static async Task Main(string[] args) rootCommand.AddCommand(archiveCommand); } + { + var pathArg = new Argument("filename", "The path of the SerializedFile").ExistingOnly(); + var fOpt = new Option(aliases: new[] { "--format", "-f" }, description: "Output format", getDefaultValue: () => OutputFormat.Text); + + var externalRefsCommand = new Command("externalrefs", "List external file references in a SerializedFile.") + { + pathArg, + fOpt, + }; + + externalRefsCommand.SetHandler( + (FileInfo fi, OutputFormat f) => Task.FromResult(SerializedFileCommands.HandleExternalRefs(fi, f)), + pathArg, fOpt); + + var objectListCommand = new Command("objectlist", "List all objects in a SerializedFile.") + { + pathArg, + fOpt, + }; + + objectListCommand.SetHandler( + (FileInfo fi, OutputFormat f) => Task.FromResult(SerializedFileCommands.HandleObjectList(fi, f)), + pathArg, fOpt); + + var serializedFileCommand = new Command("serialized-file", "Inspect a SerializedFile (scene, assets, etc.).") + { + externalRefsCommand, + objectListCommand, + }; + + serializedFileCommand.AddAlias("sf"); + + rootCommand.AddCommand(serializedFileCommand); + } + var r = await rootCommand.InvokeAsync(args); UnityFileSystem.Cleanup(); diff --git a/UnityDataTool/README.md b/UnityDataTool/README.md index b47c70a..8a871ca 100644 --- a/UnityDataTool/README.md +++ b/UnityDataTool/README.md @@ -1,80 +1,3 @@ # UnityDataTool -A command-line tool for analyzing and inspecting Unity build output—AssetBundles, Player builds, Addressables, and more. - -## Commands - -| Command | Description | -|---------|-------------| -| [`analyze`](Commands/analyze.md) | Extract data from Unity files into a SQLite database | -| [`dump`](Commands/dump.md) | Convert SerializedFiles to human-readable text | -| [`archive`](Commands/archive.md) | List or extract contents of Unity Archives | -| [`find-refs`](Commands/find-refs.md) | Trace reference chains to objects *(experimental)* | - ---- - -## Quick Start - -```bash -# Show all commands -UnityDataTool --help - -# Analyze AssetBundles into SQLite database -UnityDataTool analyze /path/to/bundles -o database.db - -# Dump a file to text format -UnityDataTool dump /path/to/file.bundle -o /output/path - -# Extract archive contents -UnityDataTool archive extract file.bundle -o contents/ - -# Find reference chains to an object -UnityDataTool find-refs database.db -n "ObjectName" -t "Texture2D" -``` - -Use `--help` with any command for details: `UnityDataTool analyze --help` - -Use `--version` to print the tool version. - - -## Installation - -### Building - -First, build the solution as described in the [main README](../README.md#how-to-build). - -The executable will be at: -``` -UnityDataTool/bin/Release/net9.0/UnityDataTool.exe -``` - -> **Tip:** Add the directory containing `UnityDataTool.exe` to your `PATH` environment variable for easy access. - -### Mac Instructions - -On Mac, publish the project to get an executable: - -**Intel Mac:** -```bash -dotnet publish UnityDataTool -c Release -r osx-x64 -p:PublishSingleFile=true -p:UseAppHost=true -``` - -**Apple Silicon Mac:** -```bash -dotnet publish UnityDataTool -c Release -r osx-arm64 -p:PublishSingleFile=true -p:UseAppHost=true -``` - -If you see a warning about `UnityFileSystemApi.dylib` not being verified, go to **System Preferences → Security & Privacy** and allow the file. - ---- - -## Related Documentation - -| Topic | Description | -|-------|-------------| -| [Analyzer Database Reference](../Analyzer/README.md) | SQLite schema, views, and extending the analyzer | -| [TextDumper Output Format](../TextDumper/README.md) | Understanding dump output | -| [ReferenceFinder Details](../ReferenceFinder/README.md) | Reference chain output format | -| [Analyze Examples](../Documentation/analyze-examples.md) | Practical database queries | -| [Comparing Builds](../Documentation/comparing-builds.md) | Strategies for build comparison | -| [Unity Content Format](../Documentation/unity-content-format.md) | TypeTrees and file formats | +See [Documentation/unitydatatool.md](../Documentation/unitydatatool.md) diff --git a/UnityDataTool/SerializedFileCommands.cs b/UnityDataTool/SerializedFileCommands.cs new file mode 100644 index 0000000..56ca0ca --- /dev/null +++ b/UnityDataTool/SerializedFileCommands.cs @@ -0,0 +1,139 @@ +using System; +using System.IO; +using System.Text.Json; +using UnityDataTools.FileSystem; + +namespace UnityDataTools.UnityDataTool; + +public static class SerializedFileCommands +{ + public static int HandleExternalRefs(FileInfo filename, OutputFormat format) + { + try + { + using var sf = UnityFileSystem.OpenSerializedFile(filename.FullName); + + if (format == OutputFormat.Json) + OutputExternalRefsJson(sf); + else + OutputExternalRefsText(sf); + } + catch (Exception err) when (err is NotSupportedException || err is FileFormatException) + { + Console.Error.WriteLine($"Error opening serialized file: {filename.FullName}"); + Console.Error.WriteLine(err.Message); + return 1; + } + + return 0; + } + + public static int HandleObjectList(FileInfo filename, OutputFormat format) + { + try + { + using var sf = UnityFileSystem.OpenSerializedFile(filename.FullName); + + if (format == OutputFormat.Json) + OutputObjectListJson(sf); + else + OutputObjectListText(sf); + } + catch (Exception err) when (err is NotSupportedException || err is FileFormatException) + { + Console.Error.WriteLine($"Error opening serialized file: {filename.FullName}"); + Console.Error.WriteLine(err.Message); + return 1; + } + + return 0; + } + + private static void OutputExternalRefsText(SerializedFile sf) + { + var refs = sf.ExternalReferences; + + for (int i = 0; i < refs.Count; i++) + { + var extRef = refs[i]; + var displayValue = !string.IsNullOrEmpty(extRef.Path) ? extRef.Path : extRef.Guid; + Console.WriteLine($"Index: {i + 1}, Path: {displayValue}"); + } + } + + private static void OutputExternalRefsJson(SerializedFile sf) + { + var refs = sf.ExternalReferences; + var jsonArray = new object[refs.Count]; + + for (int i = 0; i < refs.Count; i++) + { + var extRef = refs[i]; + jsonArray[i] = new + { + index = i + 1, + path = extRef.Path, + guid = extRef.Guid, + type = extRef.Type.ToString() + }; + } + + var json = JsonSerializer.Serialize(jsonArray, new JsonSerializerOptions { WriteIndented = true }); + Console.WriteLine(json); + } + + private static void OutputObjectListText(SerializedFile sf) + { + var objects = sf.Objects; + + // Print header + Console.WriteLine($"{"Id",-20} {"Type",-40} {"Offset",-15} {"Size",-15}"); + Console.WriteLine(new string('-', 90)); + + foreach (var obj in objects) + { + string typeName = GetTypeName(sf, obj); + Console.WriteLine($"{obj.Id,-20} {typeName,-40} {obj.Offset,-15} {obj.Size,-15}"); + } + } + + private static void OutputObjectListJson(SerializedFile sf) + { + var objects = sf.Objects; + var jsonArray = new object[objects.Count]; + + for (int i = 0; i < objects.Count; i++) + { + var obj = objects[i]; + string typeName = GetTypeName(sf, obj); + + jsonArray[i] = new + { + id = obj.Id, + typeId = obj.TypeId, + typeName = typeName, + offset = obj.Offset, + size = obj.Size + }; + } + + var json = JsonSerializer.Serialize(jsonArray, new JsonSerializerOptions { WriteIndented = true }); + Console.WriteLine(json); + } + + private static string GetTypeName(SerializedFile sf, ObjectInfo obj) + { + try + { + // Try to get type name from TypeTree first (most accurate) + var root = sf.GetTypeTreeRoot(obj.Id); + return root.Type; + } + catch + { + // Fall back to registry if TypeTree is not available + return TypeIdRegistry.GetTypeName(obj.TypeId); + } + } +} + diff --git a/UnityDataTool/UnityDataTool.csproj b/UnityDataTool/UnityDataTool.csproj index 1feecd8..4e6320e 100644 --- a/UnityDataTool/UnityDataTool.csproj +++ b/UnityDataTool/UnityDataTool.csproj @@ -4,10 +4,10 @@ Exe net9.0 latest - 1.2.1 - 1.2.1.0 - 1.2.1.0 - 1.2.1 + 1.3.0 + 1.3.0.0 + 1.3.0.0 + 1.3.0 diff --git a/UnityFileSystem/TypeIdRegistry.cs b/UnityFileSystem/TypeIdRegistry.cs new file mode 100644 index 0000000..ec4928d --- /dev/null +++ b/UnityFileSystem/TypeIdRegistry.cs @@ -0,0 +1,318 @@ +using System.Collections.Generic; + +namespace UnityDataTools.FileSystem; + +/// +/// Registry of common Unity TypeIds mapped to their type names. +/// Used as a fallback when TypeTree information is not available. +/// Reference: https://docs.unity3d.com/Manual/ClassIDReference.html +/// +public static class TypeIdRegistry +{ + private static readonly Dictionary s_KnownTypes = new() + { + { 1, "GameObject" }, + { 2, "Component" }, + { 3, "LevelGameManager" }, + { 4, "Transform" }, + { 5, "TimeManager" }, + { 6, "GlobalGameManager" }, + { 8, "Behaviour" }, + { 9, "GameManager" }, + { 11, "AudioManager" }, + { 13, "InputManager" }, + { 18, "EditorExtension" }, + { 19, "Physics2DSettings" }, + { 20, "Camera" }, + { 21, "Material" }, + { 23, "MeshRenderer" }, + { 25, "Renderer" }, + { 27, "Texture" }, + { 28, "Texture2D" }, + { 29, "OcclusionCullingSettings" }, + { 30, "GraphicsSettings" }, + { 33, "MeshFilter" }, + { 41, "OcclusionPortal" }, + { 43, "Mesh" }, + { 45, "Skybox" }, + { 47, "QualitySettings" }, + { 48, "Shader" }, + { 49, "TextAsset" }, + { 50, "Rigidbody2D" }, + { 53, "Collider2D" }, + { 54, "Rigidbody" }, + { 55, "PhysicsManager" }, + { 56, "Collider" }, + { 57, "Joint" }, + { 58, "CircleCollider2D" }, + { 59, "HingeJoint" }, + { 60, "PolygonCollider2D" }, + { 61, "BoxCollider2D" }, + { 62, "PhysicsMaterial2D" }, + { 64, "MeshCollider" }, + { 65, "BoxCollider" }, + { 66, "CompositeCollider2D" }, + { 68, "EdgeCollider2D" }, + { 70, "CapsuleCollider2D" }, + { 72, "ComputeShader" }, + { 74, "AnimationClip" }, + { 75, "ConstantForce" }, + { 78, "TagManager" }, + { 81, "AudioListener" }, + { 82, "AudioSource" }, + { 83, "AudioClip" }, + { 84, "RenderTexture" }, + { 86, "CustomRenderTexture" }, + { 89, "Cubemap" }, + { 90, "Avatar" }, + { 91, "AnimatorController" }, + { 93, "RuntimeAnimatorController" }, + { 94, "ShaderNameRegistry" }, + { 95, "Animator" }, + { 96, "TrailRenderer" }, + { 98, "DelayedCallManager" }, + { 102, "TextMesh" }, + { 104, "RenderSettings" }, + { 108, "Light" }, + { 109, "ShaderInclude" }, + { 110, "BaseAnimationTrack" }, + { 111, "Animation" }, + { 114, "MonoBehaviour" }, + { 115, "MonoScript" }, + { 116, "MonoManager" }, + { 117, "Texture3D" }, + { 118, "NewAnimationTrack" }, + { 119, "Projector" }, + { 120, "LineRenderer" }, + { 121, "Flare" }, + { 122, "Halo" }, + { 123, "LensFlare" }, + { 124, "FlareLayer" }, + { 126, "NavMeshProjectSettings" }, + { 128, "Font" }, + { 129, "PlayerSettings" }, + { 130, "NamedObject" }, + { 134, "PhysicsMaterial" }, + { 135, "SphereCollider" }, + { 136, "CapsuleCollider" }, + { 137, "SkinnedMeshRenderer" }, + { 138, "FixedJoint" }, + { 141, "BuildSettings" }, + { 142, "AssetBundle" }, + { 143, "CharacterController" }, + { 144, "CharacterJoint" }, + { 145, "SpringJoint" }, + { 146, "WheelCollider" }, + { 147, "ResourceManager" }, + { 150, "PreloadData" }, + { 152, "MovieTexture" }, + { 153, "ConfigurableJoint" }, + { 154, "TerrainCollider" }, + { 156, "TerrainData" }, + { 157, "LightmapSettings" }, + { 158, "WebCamTexture" }, + { 159, "EditorSettings" }, + { 162, "EditorUserSettings" }, + { 164, "AudioReverbFilter" }, + { 165, "AudioHighPassFilter" }, + { 166, "AudioChorusFilter" }, + { 167, "AudioReverbZone" }, + { 168, "AudioEchoFilter" }, + { 169, "AudioLowPassFilter" }, + { 170, "AudioDistortionFilter" }, + { 171, "SparseTexture" }, + { 180, "AudioBehaviour" }, + { 181, "AudioFilter" }, + { 182, "WindZone" }, + { 183, "Cloth" }, + { 184, "SubstanceArchive" }, + { 185, "ProceduralMaterial" }, + { 186, "ProceduralTexture" }, + { 187, "Texture2DArray" }, + { 188, "CubemapArray" }, + { 191, "OffMeshLink" }, + { 192, "OcclusionArea" }, + { 193, "Tree" }, + { 195, "NavMeshAgent" }, + { 196, "NavMeshSettings" }, + { 198, "ParticleSystem" }, + { 199, "ParticleSystemRenderer" }, + { 200, "ShaderVariantCollection" }, + { 205, "LODGroup" }, + { 206, "BlendTree" }, + { 207, "Motion" }, + { 208, "NavMeshObstacle" }, + { 210, "SortingGroup" }, + { 212, "SpriteRenderer" }, + { 213, "Sprite" }, + { 214, "CachedSpriteAtlas" }, + { 215, "ReflectionProbe" }, + { 218, "Terrain" }, + { 220, "LightProbeGroup" }, + { 221, "AnimatorOverrideController" }, + { 222, "CanvasRenderer" }, + { 223, "Canvas" }, + { 224, "RectTransform" }, + { 225, "CanvasGroup" }, + { 226, "BillboardAsset" }, + { 227, "BillboardRenderer" }, + { 228, "SpeedTreeWindAsset" }, + { 229, "AnchoredJoint2D" }, + { 230, "Joint2D" }, + { 231, "SpringJoint2D" }, + { 232, "DistanceJoint2D" }, + { 233, "HingeJoint2D" }, + { 234, "SliderJoint2D" }, + { 235, "WheelJoint2D" }, + { 236, "ClusterInputManager" }, + { 237, "BaseVideoTexture" }, + { 238, "NavMeshData" }, + { 240, "AudioMixer" }, + { 241, "AudioMixerController" }, + { 243, "AudioMixerGroupController" }, + { 244, "AudioMixerEffectController" }, + { 245, "AudioMixerSnapshotController" }, + { 246, "PhysicsUpdateBehaviour2D" }, + { 247, "ConstantForce2D" }, + { 248, "Effector2D" }, + { 249, "AreaEffector2D" }, + { 250, "PointEffector2D" }, + { 251, "PlatformEffector2D" }, + { 252, "SurfaceEffector2D" }, + { 253, "BuoyancyEffector2D" }, + { 254, "RelativeJoint2D" }, + { 255, "FixedJoint2D" }, + { 256, "FrictionJoint2D" }, + { 257, "TargetJoint2D" }, + { 258, "LightProbes" }, + { 259, "LightProbeProxyVolume" }, + { 271, "SampleClip" }, + { 272, "AudioMixerSnapshot" }, + { 273, "AudioMixerGroup" }, + { 290, "AssetBundleManifest" }, + { 300, "RuntimeInitializeOnLoadManager" }, + { 310, "UnityConnectSettings" }, + { 319, "AvatarMask" }, + { 320, "PlayableDirector" }, + { 328, "VideoPlayer" }, + { 329, "VideoClip" }, + { 330, "ParticleSystemForceField" }, + { 331, "SpriteMask" }, + { 363, "OcclusionCullingData" }, + { 900, "MarshallingTestObject" }, + { 1001, "PrefabInstance" }, + { 1002, "EditorExtensionImpl" }, + { 1026, "HierarchyState" }, + { 1028, "AssetMetaData" }, + { 1029, "DefaultAsset" }, + { 1032, "SceneAsset" }, + { 1045, "EditorBuildSettings" }, + { 1048, "InspectorExpandedState" }, + { 1049, "AnnotationManager" }, + { 1051, "EditorUserBuildSettings" }, + { 1101, "AnimatorStateTransition" }, + { 1102, "AnimatorState" }, + { 1105, "HumanTemplate" }, + { 1107, "AnimatorStateMachine" }, + { 1108, "PreviewAnimationClip" }, + { 1109, "AnimatorTransition" }, + { 1111, "AnimatorTransitionBase" }, + { 1113, "LightmapParameters" }, + { 1120, "LightingDataAsset" }, + { 1125, "BuildReport" }, + { 1126, "PackedAssets" }, + { 100000, "int" }, + { 100001, "bool" }, + { 100002, "float" }, + { 100003, "MonoObject" }, + { 100004, "Collision" }, + { 100005, "Vector3f" }, + { 100006, "RootMotionData" }, + { 100007, "Collision2D" }, + { 100008, "AudioMixerLiveUpdateFloat" }, + { 100009, "AudioMixerLiveUpdateBool" }, + { 100010, "Polygon2D" }, + { 100011, "void" }, + { 19719996, "TilemapCollider2D" }, + { 41386430, "ImportLog" }, + { 55640938, "GraphicsStateCollection" }, + { 73398921, "VFXRenderer" }, + { 156049354, "Grid" }, + { 156483287, "ScenesUsingAssets" }, + { 171741748, "ArticulationBody" }, + { 181963792, "Preset" }, + { 285090594, "IConstraint" }, + { 355983997, "AudioResource" }, + { 369655926, "AssetImportInProgressProxy" }, + { 382020655, "PluginBuildInfo" }, + { 387306366, "MemorySettings" }, + { 426301858, "EditorProjectAccess" }, + { 483693784, "TilemapRenderer" }, + { 612988286, "SpriteAtlasAsset" }, + { 638013454, "SpriteAtlasDatabase" }, + { 641289076, "AudioBuildInfo" }, + { 644342135, "CachedSpriteAtlasRuntimeData" }, + { 655991488, "MultiplayerManager" }, + { 662584278, "AssemblyDefinitionReferenceAsset" }, + { 668709126, "BuiltAssetBundleInfoSet" }, + { 687078895, "SpriteAtlas" }, + { 702665669, "DifferentMarshallingTestObject" }, + { 825902497, "RayTracingShader" }, + { 850595691, "LightingSettings" }, + { 877146078, "PlatformModuleSetup" }, + { 890905787, "VersionControlSettings" }, + { 893571522, "CustomCollider2D" }, + { 895512359, "AimConstraint" }, + { 937362698, "VFXManager" }, + { 947337230, "RoslynAnalyzerConfigAsset" }, + { 954905827, "RuleSetFileAsset" }, + { 994735392, "VisualEffectSubgraph" }, + { 994735403, "VisualEffectSubgraphOperator" }, + { 994735404, "VisualEffectSubgraphBlock" }, + { 1001480554, "Prefab" }, + { 1114811875, "ReferencesArtifactGenerator" }, + { 1152215463, "AssemblyDefinitionAsset" }, + { 1154873562, "SceneVisibilityState" }, + { 1183024399, "LookAtConstraint" }, + { 1233149941, "AudioContainerElement" }, + { 1268269756, "GameObjectRecorder" }, + { 1307931743, "AudioRandomContainer" }, + { 1325145578, "LightingDataAssetParent" }, + { 1386491679, "PresetManager" }, + { 1403656975, "StreamingManager" }, + { 1480428607, "LowerResBlitTexture" }, + { 1521398425, "VideoBuildInfo" }, + { 1542919678, "StreamingController" }, + { 1557264870, "ShaderContainer" }, + { 1597193336, "RoslynAdditionalFileAsset" }, + { 1652712579, "MultiplayerRolesData" }, + { 1660057539, "SceneRoots" }, + { 1731078267, "BrokenPrefabAsset" }, + { 1740304944, "VulkanDeviceFilterLists" }, + { 1742807556, "GridLayout" }, + { 1773428102, "ParentConstraint" }, + { 1818360608, "PositionConstraint" }, + { 1818360609, "RotationConstraint" }, + { 1818360610, "ScaleConstraint" }, + { 1839735485, "Tilemap" }, + { 1896753125, "PackageManifest" }, + { 1931382933, "UIRenderer" }, + { 1953259897, "TerrainLayer" }, + { 1971053207, "SpriteShapeRenderer" }, + { 2058629509, "VisualEffectAsset" }, + { 2058629511, "VisualEffectResource" }, + { 2059678085, "VisualEffectObject" }, + { 2083052967, "VisualEffect" }, + { 2083778819, "LocalizationAsset" }, + }; + + /// The Unity TypeId + /// The type name or TypeId as string if unknown + public static string GetTypeName(int typeId) + { + return s_KnownTypes.TryGetValue(typeId, out var name) + ? name + : typeId.ToString(); + } +} +