diff --git a/AGENTS.md b/AGENTS.md index 93fd1f1..3e8ca79 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -78,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" ``` @@ -139,13 +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) -- `Documentation/` - Command documentation (command-analyze.md, command-dump.md, command-archive.md, command-find-refs.md) +- `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**: diff --git a/Analyzer/SQLite/Handlers/PackedAssetsHandler.cs b/Analyzer/SQLite/Handlers/PackedAssetsHandler.cs index c2f62db..78268b4 100644 --- a/Analyzer/SQLite/Handlers/PackedAssetsHandler.cs +++ b/Analyzer/SQLite/Handlers/PackedAssetsHandler.cs @@ -64,7 +64,7 @@ public void Init(SqliteConnection db) 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; @@ -96,6 +96,11 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s 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; 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/SerializedObjects/BuildReport.cs b/Analyzer/SerializedObjects/BuildReport.cs index ae274e9..6dd413d 100644 --- a/Analyzer/SerializedObjects/BuildReport.cs +++ b/Analyzer/SerializedObjects/BuildReport.cs @@ -120,7 +120,7 @@ public static string GetBuildTypeString(int buildType) { 1 => "Player", 2 => "AssetBundle", - 3 => "Player, AssetBundle", + 3 => "ContentDirectory", _ => buildType.ToString() }; } 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/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 index 94b5344..0f34eae 100644 --- a/Documentation/unitydatatool.md +++ b/Documentation/unitydatatool.md @@ -9,6 +9,7 @@ A command-line tool for analyzing and inspecting Unity build output—AssetBundl | [`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)* | --- @@ -28,6 +29,10 @@ 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" ``` 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/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/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/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/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(); + } +} +