diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8b92c61 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,316 @@ +# see http://editorconfig.org/ for docs on this file + +root = true + +# _ _ _ +# | | | | (_) +# ___ ___ _ __ ___ _ __ ___ ___ _ __ ___ ___| |_| |_ _ _ __ __ _ ___ +# / __/ _ \| '_ ` _ \| '_ ` _ \ / _ \| '_ \ / __|/ _ \ __| __| | '_ \ / _` / __| +# | (_| (_) | | | | | | | | | | | (_) | | | | \__ \ __/ |_| |_| | | | | (_| \__ \ +# \___\___/|_| |_| |_|_| |_| |_|\___/|_| |_| |___/\___|\__|\__|_|_| |_|\__, |___/ +# __/ | +# |___/ + +[*] + +# sanity across platforms +end_of_line = lf + +# this is our general standard: exceptions permitted only when a) required by the format or b) strong de facto convention +indent_style = space +indent_size = 4 + +# prevent files with BOMs on them coming into the repo +charset = utf-8 + +# other common settings +trim_trailing_whitespace = true +insert_final_newline = true + +cpp_space_pointer_reference_alignment = left + +# _ _ +# (_) | | +# _____ _____ _ __ _ __ _ __| | ___ ___ +# / _ \ \ / / _ \ '__| '__| |/ _` |/ _ \/ __| +# | (_) \ V / __/ | | | | | (_| | __/\__ \ +# \___/ \_/ \___|_| |_| |_|\__,_|\___||___/ +# +# + +[{Makefile,makefile}] +indent_style = tab + +[*.{md,markdown,mdx}] +# trailing whitespace is significant in markdown (bad choice, bad!) +trim_trailing_whitespace = false + +# crlf because tool expectations (based on experimentation) +[*.{bat,cmd,xaml,tt,t4,ttinclude}] +end_of_line = crlf + +[*.{json,asmdef}] +indent_size = 2 + +[*.{yaml,yml}] +indent_size = 2 + +# msbuild-related files (these are usually not tool-authored) +[*.{props,targets,msbuildproj,proj}] +indent_size = 2 + +### visual studio + +# these settings are based on experiments to see how vs will modify a file after it has been +# manually edited. the settings are meant to match what vs does to minimize unnecessary diffs. + +# related notes from research: +# +# 1. rider tends to preserve existing file settings, but we must go with the more strict vs. +# +# 2. file-new templates in vs, rider, and `dotnet new` contain bom's (byte order markers) and +# crlf. crlf we must preserve because vs, but bom's must be burned with fire. all editors are +# fine with this. + +[*.sln] +indent_style = tab +end_of_line = crlf +insert_final_newline = false + +[*.{vcxproj,vcxproj.filters}] +indent_size = 2 +end_of_line = crlf +insert_final_newline = false + +[*.vcproj] +indent_style = tab +end_of_line = crlf + +# csproj is a bit more flexible, in part because VS does some preservation of whitespace. however, +# lines it adds will get crlf's, and if the rest of the file has lf, we'll get a mixed file. so we +# must set the whole thing to be crlf. +[*.csproj] +indent_size = 2 +end_of_line = crlf + + +# __ _ _ +# / _| | | | | +# | |_ ___ _ __ _ __ ___ __ _| |_ _ __ _ _| | ___ ___ +# | _/ _ \| '__| '_ ` _ \ / _` | __| | '__| | | | |/ _ \/ __| +# | || (_) | | | | | | | | (_| | |_ | | | |_| | | __/\__ \ +# |_| \___/|_| |_| |_| |_|\__,_|\__| |_| \__,_|_|\___||___/ +# +# + +# http://go/format +# https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-format + + +[*.cs] + +# The entire monorepo has a single standard. +# +# if you want to propose changes to the standard, raise it with the people assigned to .editorconfig in +# .github/UNITY_CODEOWNERS. + +# ___ ___ __ __ __ ___ +# | | |__| | | |__ /__` |__) /\ / ` |__ +# |/\| | | | | |___ .__/ | /~~\ \__, |___ + +# whitespace-only format uses options under IDE0055 (https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0055) +# +# IDE0055 is made up of these formatting options: +# - https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/csharp-formatting-options +# - https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/dotnet-formatting-options + +# to reformat code with these rules, use `dotnet format whitespace --folder`. the rules are exclusively about +# code structure and do not require symbol awareness. they can be run on any file without needing a project or compile +# to be done first. + +# newline options +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# indentation options +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = one_less_than_current +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents_when_block = false + +# spacing +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_after_comma = true +csharp_space_before_comma = false +csharp_space_after_dot = false +csharp_space_before_dot = false +csharp_space_after_semicolon_in_for_statement = true +csharp_space_before_semicolon_in_for_statement = false +csharp_space_around_declaration_statements = false +csharp_space_before_open_square_brackets = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_square_brackets = false + +# wrap options +csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = true +max_line_length = 120 + +# using directives +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false + +# __ ___ ___ +# /__` | \ / | |__ +# .__/ | | |___ |___ + +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ + +# Code-style naming rules +# https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/naming-rules +# A. dotnet_naming_symbols: specify a symbol group +# B. dotnet_naming_style: specify a naming style +# C. dotnet_naming_rule: specify a rule that applies a naming style to a symbol group + +# 1. General symbol rules +# 1.1 Namespaces, classes, structs, enumerations, methods, and delegates: PascalCase +dotnet_naming_symbols.majority_of_symbols.applicable_kinds = namespace, class, struct, enum, method, delegate +dotnet_naming_symbols.majority_of_symbols.applicable_accessibilities = * +# 1.2 Modifier preferences +dotnet_style_require_accessibility_modifiers = omit_if_default:suggestion +dotnet_style_readonly_field = true:suggestion + +# NOTE camelCase vs PascalCase for public fields/properties/events is the only different between +# UnityEngine/UnityEditor and Unity (non-UnityEngine/UnityEditor to be exact) namespace conventions. +# Search for "NOTE Unity namespace" to find the appropriate places where to do the adjustments. +dotnet_naming_style.camel_case_style.capitalization = camel_case +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +dotnet_naming_rule.majority_of_symbols_should_be_pascal_case.symbols = majority_of_symbols # The symbol group that the rule applies to +dotnet_naming_rule.majority_of_symbols_should_be_pascal_case.style = pascal_case_style # The naming style to associate with the rule +dotnet_naming_rule.majority_of_symbols_should_be_pascal_case.severity = suggestion # The severity for enforcing the convention + +# 1.2 Interfaces: IPascalCase +dotnet_naming_symbols.interfaces.applicable_kinds = interface +dotnet_naming_symbols.interfaces.applicable_accessibilities = * + +dotnet_naming_style.ipascal_case_style.capitalization = pascal_case +dotnet_naming_style.ipascal_case_style.required_prefix = I + +dotnet_naming_rule.interfaces_should_begin_with_i.symbols = interfaces +dotnet_naming_rule.interfaces_should_begin_with_i.style = ipascal_case_style +dotnet_naming_rule.interfaces_should_begin_with_i.severity = suggestion + +# 1.3 Type parameters: TPascalCase +dotnet_naming_symbols.type_parameters.applicable_kinds = type_parameter +dotnet_naming_symbols.type_parameters.applicable_accessibilities = * + +dotnet_naming_style.tpascal_case_style.capitalization = pascal_case +dotnet_naming_style.tpascal_case_style.required_prefix = T + +dotnet_naming_rule.type_parameters_should_beging_with_t.symbols = type_parameters +dotnet_naming_rule.type_parameters_should_beging_with_t.style = tpascal_case_style +dotnet_naming_rule.type_parameters_should_beging_with_t.severity = suggestion + +# 2. Public fields, including events, all properties: + +# Public fields and properties case depends on the namespace and can't be generalized. +# Public fields and properties in UnityEngine and UnityEditor namespaces should use camelCase, +# and they should PascalCase in other namespaces. + +# To enforce this another .editorconfig file will be needed in the subfolders containing either +# UnityEngine and UnityEditor code or other namespace code. +# See /Modules/HierarchyCore/.editorconfig for an example of overriden rules for specific namespaces. + +# 2.1 fields +dotnet_naming_symbols.public_fields.applicable_kinds = field, event +dotnet_naming_symbols.public_fields.applicable_accessibilities = public + +dotnet_naming_rule.public_fields_use_correct_case.symbols = public_fields +dotnet_naming_rule.public_fields_use_correct_case.style = pascal_case_style +dotnet_naming_rule.public_fields_use_correct_case.severity = none + +# 2.2 properties +dotnet_naming_symbols.properties.applicable_kinds = property +dotnet_naming_symbols.properties.applicable_accessibilities = * + +dotnet_naming_rule.properties_use_correct_case.symbols = properties +dotnet_naming_rule.properties_use_correct_case.style = pascal_case_style +dotnet_naming_rule.properties_use_correct_case.severity = none + +# 3. Non-public fields, including events: m_PascalCase +dotnet_naming_symbols.nonpublic_fields.applicable_kinds = field, event +dotnet_naming_symbols.nonpublic_fields.applicable_accessibilities = internal, private, protected, protected_internal, private_protected + +dotnet_naming_style.m_pascal_case_style.capitalization = pascal_case +dotnet_naming_style.m_pascal_case_style.required_prefix = m_ + +dotnet_naming_rule.nonpublic_fields_must_be_prefixed.symbols = nonpublic_fields +dotnet_naming_rule.nonpublic_fields_must_be_prefixed.style = m_pascal_case_style +dotnet_naming_rule.nonpublic_fields_must_be_prefixed.severity = suggestion + +# 4. Non-public static fields: s_PascalCase +dotnet_naming_symbols.nonpublic_static_fields.applicable_kinds = field, event +dotnet_naming_symbols.nonpublic_static_fields.applicable_accessibilities = internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.nonpublic_static_fields.required_modifiers = static + +dotnet_naming_style.s_pascal_case_style.capitalization = pascal_case +dotnet_naming_style.s_pascal_case_style.required_prefix = s_ + +dotnet_naming_rule.nonpublic_static_fields_must_be_prefixed.symbols = nonpublic_static_fields +dotnet_naming_rule.nonpublic_static_fields_must_be_prefixed.style = s_pascal_case_style +dotnet_naming_rule.nonpublic_static_fields_must_be_prefixed.severity = suggestion + +# 5. Non-public const fields: k_PascalCase +dotnet_naming_symbols.nonpublic_const_fields.applicable_kinds = field, event +dotnet_naming_symbols.nonpublic_const_fields.applicable_accessibilities = internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.nonpublic_const_fields.required_modifiers = const + +dotnet_naming_style.k_pascal_case_style.capitalization = pascal_case +dotnet_naming_style.k_pascal_case_style.required_prefix = k_ + +dotnet_naming_rule.nonpublic_const_fields_must_be_prefixed.symbols = nonpublic_const_fields +dotnet_naming_rule.nonpublic_const_fields_must_be_prefixed.style = k_pascal_case_style +dotnet_naming_rule.nonpublic_const_fields_must_be_prefixed.severity = suggestion + +# 6. Non-public static readonly fields: k_PascalCase +dotnet_naming_symbols.nonpublic_static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.nonpublic_static_readonly_fields.applicable_accessibilities = internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.nonpublic_static_readonly_fields.required_modifiers = static, readonly + +dotnet_naming_rule.nonpublic_static_readonly_fields_must_be_prefixed.symbols = nonpublic_static_readonly_fields +dotnet_naming_rule.nonpublic_static_readonly_fields_must_be_prefixed.style = k_pascal_case_style +dotnet_naming_rule.nonpublic_static_readonly_fields_must_be_prefixed.severity = suggestion + +# 7. Parameters and local variables: camelCase +# 7.1 parameters +dotnet_naming_symbols.parameters.applicable_kinds = parameter + +dotnet_naming_rule.parameters_must_be_camel_case.symbols = parameters +dotnet_naming_rule.parameters_must_be_camel_case.style = camel_case_style +dotnet_naming_rule.parameters_must_be_camel_case.severity = suggestion + +# 7.2 local variables +dotnet_naming_symbols.local_variables.applicable_kinds = local + +dotnet_naming_rule.local_variables_must_be_camel_case.symbols = local_variables +dotnet_naming_rule.local_variables_must_be_camel_case.style = camel_case_style +dotnet_naming_rule.local_variables_must_be_camel_case.severity = suggestion diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9809c78 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,23 @@ +* text=auto eol=lf + +assetbundle binary +scenes binary +level* binary +*.dll binary +*.dylib binary +*.so binary +*.wav binary +*.fbx binary +*.tif binary +*.jpg binary + + +# vs can handle these as lf, but really wants them as crlf +*.vcproj eol=crlf +*.vcxproj eol=crlf +*.vcxproj.filters eol=crlf +*.csproj eol=crlf +*.props eol=crlf +*.targets eol=crlf +*.sln eol=crlf +*.sln.template eol=crlf diff --git a/.github/workflows/.build.yml b/.github/workflows/.build.yml new file mode 100644 index 0000000..9f2bdcf --- /dev/null +++ b/.github/workflows/.build.yml @@ -0,0 +1,57 @@ +name: Build UnityDataTool + +on: + workflow_dispatch: + inputs: + environment: + description: 'Build environment' + default: 'release' + required: true + type: choice + options: + - debug + - release + push: + branches: + - main + +env: + environment: ${{ inputs.environment || 'release' }} + +jobs: + build: + strategy: + matrix: + os: [windows, macos] + arch: [x64, arm64] + exclude: + - os: windows + arch: arm64 + - os: macos + arch: x64 + + runs-on: ${{ matrix.os }}-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + + - name: Publish UnityDataTool (${{ matrix.os }}-${{ matrix.arch }}-${{ env.environment }}) + run: > + dotnet publish UnityDataTool + -c ${{ env.environment }} + -a ${{ matrix.arch }} + -p:PublishSingleFile=true + -p:UseAppHost=true + -o publish/${{ matrix.os }}/${{ matrix.arch }}-${{ env.environment }} + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: UnityDataTool-${{ matrix.os }}-${{ matrix.arch }}-${{ env.environment }} + path: publish/${{ matrix.os }}/${{ matrix.arch }}-${{ env.environment }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..72cac53 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,58 @@ +name: Test UnityDataTools + +on: + workflow_dispatch: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + strategy: + matrix: + os: [windows, macos] + arch: [x64, arm64] + exclude: + - os: windows + arch: arm64 + - os: macos + arch: x64 + fail-fast: false + + runs-on: ${{ matrix.os }}-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + + - name: Restore dependencies + run: dotnet restore + + - name: Build solution + run: dotnet build -c Release --no-restore + + - name: Run UnityFileSystem.Tests + run: dotnet test UnityFileSystem.Tests/UnityFileSystem.Tests.csproj -c Release --no-build --verbosity normal --logger "trx;LogFileName=UnityFileSystem.Tests.trx" + + - name: Run Analyzer.Tests + run: dotnet test Analyzer.Tests/Analyzer.Tests.csproj -c Release --no-build --verbosity normal --logger "trx;LogFileName=Analyzer.Tests.trx" + + - name: Run UnityDataTool.Tests + run: dotnet test UnityDataTool.Tests/UnityDataTool.Tests.csproj -c Release --no-build --verbosity normal --logger "trx;LogFileName=UnityDataTool.Tests.trx" + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-${{ matrix.os }}-${{ matrix.arch }} + path: | + **/TestResults/*.trx + retention-days: 30 diff --git a/.gitignore b/.gitignore index aa59e68..e71a9f7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ [Tt]emp/ [Ll]og/ [Ll]ogs/ +[Pp]ublish/ **/*.csproj.user **/*.suo @@ -27,6 +28,9 @@ **/.idea/ **/launchSettings.json **/PublishProfiles/ +**/*.args.json + +.claude UnityFileSystemTestData/AssetBundles/ UnityFileSystemTestData/**/*.csproj @@ -34,3 +38,5 @@ UnityFileSystemTestData/**/*.sln UnityFileSystemTestData/ProjectSettings/ UnityFileSystemTestData/UserSettings/ UnityFileSystemTestData/Packages/ +*.db +*.csv diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3e8ca79 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,260 @@ +# AGENTS.md + +This file provides guidance to AI agents when working with code in this repository. + +## Project Overview + +UnityDataTools is a .NET 9.0 command-line tool for analyzing Unity build output (AssetBundles, Player builds, Addressables). It extracts data from Unity's proprietary binary formats into SQLite databases and human-readable text files. The tool showcases the UnityFileSystemApi native library and serves as both a production tool and reference implementation. + +## Common Commands + +### Building +```bash +# Build entire solution in Release mode +dotnet build -c Release + +# Build from solution file +dotnet build UnityDataTools.sln -c Release + +# Build specific project +dotnet build UnityDataTool/UnityDataTool.csproj -c Release +``` + +Output location (Windows): `UnityDataTool\bin\Release\net9.0\UnityDataTool.exe` + +### Publishing (Mac-specific) +```bash +# Intel Mac +dotnet publish UnityDataTool -c Release -r osx-x64 -p:PublishSingleFile=true -p:UseAppHost=true + +# Apple Silicon Mac +dotnet publish UnityDataTool -c Release -r osx-arm64 -p:PublishSingleFile=true -p:UseAppHost=true +``` + +### Testing +```bash +# Run all tests +dotnet test + +# Run tests for specific project +dotnet test UnityFileSystem.Tests/UnityFileSystem.Tests.csproj +dotnet test Analyzer.Tests/Analyzer.Tests.csproj +dotnet test UnityDataTool.Tests/UnityDataTool.Tests.csproj + +# Run tests with filter +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 +UnityDataTool --help + +# Analyze AssetBundles into SQLite database +UnityDataTool analyze /path/to/bundles -o database.db + +# Dump binary 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 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" +``` + +## Architecture + +### Component Hierarchy +``` +UnityDataTool (CLI executable) +├── Analyzer → SQLite database generation +├── TextDumper → Human-readable text output +├── ReferenceFinder → Object reference chain tracing +└── UnityFileSystem → C# wrapper for native library + └── UnityFileSystemApi (native .dll/.dylib/.so) +``` + +### Key Architectural Patterns + +**Native Interop**: UnityFileSystem wraps UnityFileSystemApi (native library from Unity Editor) via P/Invoke in `DllWrapper.cs`. The native library reads Unity Archive and SerializedFile formats. + +**TypeTree Navigation**: Unity binary files contain TypeTrees that describe object serialization. The `RandomAccessReader` class navigates these trees like property accessors: `reader["m_Name"].GetValue()`. This enables the tool to interpret objects without hardcoded type knowledge. + +**Parser Pattern**: `ISQLiteFileParser` interface allows multiple parsers to handle different file formats: +- `SerializedFileParser` - Unity binary files (AssetBundles, Player data) +- `AddressablesBuildLayoutParser` - JSON build reports + +**Handler Registry**: Type-specific handlers extract specialized properties for Unity object types. Handlers implement `ISQLiteHandler` and are registered in `SerializedFileSQLiteWriter.m_Handlers`: +- `MeshHandler` - vertices, indices, bones, blend shapes +- `Texture2DHandler` - width, height, format, mipmaps +- `ShaderHandler` - variants, keywords, subprograms +- `AudioClipHandler` - compression, channels, frequency +- `AnimationClipHandler` - legacy flag, events +- `AssetBundleHandler` - dependencies, preload data +- `PreloadDataHandler` - preloaded assets + +**SQL Schema Resources**: Each handler has an embedded `.sql` resource file defining its tables and views (e.g., `Analyzer/SQLite/Resources/Mesh.sql`). Views join type-specific tables with the base `objects` table. + +**Command Pattern**: SQL operations are encapsulated in classes derived from `AbstractCommand` with `CreateCommand()`, `SetValue()`, `ExecuteNonQuery()` methods. + +### Data Flow (Analyze Command) + +1. `Program.cs` → `HandleAnalyze()` → `AnalyzerTool.Analyze()` +2. AnalyzerTool finds files matching search pattern +3. For each file, parsers are tried in order (JSON first, then SerializedFile) +4. `SerializedFileParser.ProcessFile()`: + - Checks for Unity Archive signature → calls `MountArchive()` + - Otherwise treats as SerializedFile → calls `OpenSerializedFile()` +5. `SerializedFileSQLiteWriter.WriteSerializedFile()`: + - Iterates through `sf.Objects` + - Gets TypeTree via `sf.GetTypeTreeRoot(objectId)` + - Creates `RandomAccessReader` to navigate properties + - Looks up type-specific handler in `m_Handlers` dictionary + - Handler extracts specialized properties (e.g., MeshHandler reads vertex count) + - Writes to `objects` table + type-specific table (e.g., `meshes`) + - Optionally processes PPtrs (references) and calculates CRC32 +6. SQLiteWriter finalizes database with indexes and views + +### Important Files + +**Entry Points**: +- `UnityDataTool/Program.cs` - CLI using System.CommandLine +- `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**: +- `Analyzer/AnalyzerTool.cs` - Main API entry point +- `Analyzer/SQLite/SQLiteWriter.cs` - Base class for database writers +- `Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs` - Handler registration +- `Analyzer/SQLite/Writers/AddressablesBuildLayoutSQLWriter.cs` - JSON report processing +- `Analyzer/SQLite/Handlers/` - Type-specific extractors +- `Analyzer/SerializedObjects/` - RandomAccessReader-based property readers +- `Analyzer/SQLite/Resources/` - SQL DDL schema files + +**TextDumper**: +- `TextDumper/TextDumperTool.cs` - Converts binary to YAML-like text + +**ReferenceFinder**: +- `ReferenceFinder/ReferenceFinderTool.cs` - Traces object dependency chains + +## Extending the Tool + +### Adding New Unity Type Support + +1. Create handler class implementing `ISQLiteHandler`: + ``` + Analyzer/SQLite/Handlers/FooHandler.cs + ``` + +2. Create reader class using RandomAccessReader: + ``` + Analyzer/SerializedObjects/Foo.cs + ``` + +3. Register handler in `SerializedFileSQLiteWriter.cs`: + ```csharp + m_Handlers["Foo"] = new FooHandler(); + ``` + +4. Create SQL schema resource: + ``` + Analyzer/SQLite/Resources/Foo.sql + ``` + Define tables (e.g., `foos`) and views (e.g., `foo_view` joining `objects` and `foos`) + +5. Reference the schema in handler's GetResourceName() method + +### Adding New File Format Support + +1. Create parser implementing `ISQLiteFileParser` +2. Create writer derived from `SQLiteWriter` +3. Add parser to `AnalyzerTool.parsers` list +4. Create SQL schema and Command classes as needed + +Example: Addressables support uses `AddressablesBuildLayoutParser` + `AddressablesBuildLayoutSQLWriter` to parse JSON build reports. + +## Important Concepts + +### TypeTrees +TypeTrees describe how Unity objects are serialized (property names, types, offsets). They enable: +- Backward compatibility - reading files from different Unity versions +- Generic parsing without hardcoded type definitions +- Support for custom MonoBehaviours/ScriptableObjects + +**Critical**: Player builds exclude TypeTrees by default for performance. To analyze Player data, enable the "ForceAlwaysWriteTypeTrees" diagnostic switch during build. + +### File Formats +- **Unity Archive** - Container format (AssetBundles, .data files). Can be mounted as virtual filesystem. +- **SerializedFile** - Binary format storing Unity objects with TypeTree metadata. +- **Addressables BuildLayout** - JSON build report (buildlogreport.json, AddressablesReport.json) + +### Database Views +The SQLite output uses views extensively to join base `objects` table with type-specific tables: +- `object_view` - All objects with basic properties +- `mesh_view` - Objects + mesh-specific columns +- `texture_view` - Objects + texture-specific columns +- `shader_view` - Objects + shader-specific columns +- `view_breakdown_by_type` - Aggregated size by type +- `view_potential_duplicates` - Assets included multiple times +- `asset_view` - Explicitly assigned assets only +- `shader_keyword_ratios` - Keyword variant analysis + +See `Documentation/analyzer.md` and `Documentation/addressables-build-reports.md` for complete database schema documentation. + +### Common Issues + +**TypeTree Errors**: "Invalid object id" during analyze means SerializedFile lacks TypeTrees. Enable ForceAlwaysWriteTypeTrees or use files built with TypeTrees. + +**File Loading Warnings**: "Failed to load... File may be corrupted" is normal for non-Unity files in analyzed directories. Use `-p` search pattern to filter (e.g., `-p "*.bundle"`). + +**SQL UNIQUE Constraint Errors**: Occurs when same SerializedFile name appears in multiple archives. This happens when analyzing multiple builds in same directory or using AssetBundle variants. See `Documentation/comparing-builds.md` for solutions. + +**Mac Security**: "UnityFileSystemApi.dylib cannot be opened" - Open System Preferences → Security & Privacy and allow the library. + +## Native Library (UnityFileSystemApi) + +The native library is included for Windows, Mac, and Linux in `UnityFileSystem/` directory. It's backward compatible and reads data files from most Unity versions. + +To use a specific Unity version's library: +1. Find library in Unity Editor installation: `{UnityEditor}/Data/Tools/` +2. Copy to `UnityDataTool/UnityFileSystem/`: + - Windows: `UnityFileSystemApi.dll` + - Mac: `UnityFileSystemApi.dylib` + - Linux: `UnityFileSystemApi.so` +3. Rebuild the tool + +## Testing Data + +UnityFileSystemTestData is a Unity project that generates test data for the test suites. TestCommon provides shared test utilities. diff --git a/Analyzer.Tests/Analyzer.Tests.csproj b/Analyzer.Tests/Analyzer.Tests.csproj index dd740d0..e1ad7f2 100644 --- a/Analyzer.Tests/Analyzer.Tests.csproj +++ b/Analyzer.Tests/Analyzer.Tests.csproj @@ -1,11 +1,21 @@ - net6.0 + net9.0 enable disable false + + latest + + + + AnyCPU + + + + AnyCPU diff --git a/Analyzer.Tests/ExpectedDataGenerator.cs b/Analyzer.Tests/ExpectedDataGenerator.cs index 17b4932..33c386c 100644 --- a/Analyzer.Tests/ExpectedDataGenerator.cs +++ b/Analyzer.Tests/ExpectedDataGenerator.cs @@ -1,3 +1,6 @@ +using System; +using System.IO; +using System.Linq; using UnityDataTools.Analyzer.SerializedObjects; using UnityDataTools.FileSystem; using UnityDataTools.FileSystem.TypeTreeReaders; @@ -13,8 +16,8 @@ public static void Generate(Context context) using var archive = UnityFileSystem.MountArchive(Path.Combine(context.UnityDataFolder, "assetbundle"), "/"); using var serializedFile = UnityFileSystem.OpenSerializedFile("/CAB-5d40f7cad7c871cf2ad2af19ac542994"); - using var fileReader = new UnityFileReader("archive:/CAB-5d40f7cad7c871cf2ad2af19ac542994", 1024*1024); - + using var fileReader = new UnityFileReader("archive:/CAB-5d40f7cad7c871cf2ad2af19ac542994", 1024 * 1024); + AddObject(-4850512016903265157, "Shader", serializedFile, fileReader, context, Shader.Read); AddObject(-9023202112035587373, "Texture1", serializedFile, fileReader, context, Texture2D.Read); AddObject(404836592933730457, "Texture2", serializedFile, fileReader, context, Texture2D.Read); @@ -22,7 +25,7 @@ public static void Generate(Context context) AddObject(4693305862354978555, "Mesh", serializedFile, fileReader, context, Mesh.Read); AddObject(-8074603400156879931, "AudioClip", serializedFile, fileReader, context, AudioClip.Read); AddObject(1, "AssetBundle", serializedFile, fileReader, context, AssetBundle.Read); - + var csprojFolder = Directory.GetParent(context.TestDataFolder).Parent.Parent.Parent.FullName; var outputFolder = Path.Combine(csprojFolder, "ExpectedData", context.UnityDataVersion); @@ -36,7 +39,7 @@ static void AddObject(long id, string name, SerializedFile serializedFile, Un var node = serializedFile.GetTypeTreeRoot(objectInfo.Id); var reader = new RandomAccessReader(serializedFile, node, fileReader, objectInfo.Offset); var obj = creator(reader); - + context.ExpectedData.Add(name, obj); } } diff --git a/Analyzer.Tests/SerializedObjectsTests.cs b/Analyzer.Tests/SerializedObjectsTests.cs index 1b58620..43034b2 100644 --- a/Analyzer.Tests/SerializedObjectsTests.cs +++ b/Analyzer.Tests/SerializedObjectsTests.cs @@ -1,7 +1,10 @@ +using System; +using System.IO; +using System.Linq; using NUnit.Framework; +using UnityDataTools.Analyzer.SerializedObjects; using UnityDataTools.FileSystem; using UnityDataTools.FileSystem.TypeTreeReaders; -using UnityDataTools.Analyzer.SerializedObjects; using UnityDataTools.TestCommon; using UnityDataTools.UnityDataTool.Tests; @@ -18,7 +21,7 @@ public class SerializedObjectsTests : AssetBundleTestFixture public SerializedObjectsTests(Context context) : base(context) { } - + protected override void OnLoadExpectedData(Context context) { // Uncomment to regenerate expected data. @@ -33,9 +36,9 @@ public void OneTimeSetup() var path = Path.Combine(Context.UnityDataFolder, "assetbundle"); m_Archive = UnityFileSystem.MountArchive(path, "archive:/"); m_SerializedFile = UnityFileSystem.OpenSerializedFile("archive:/CAB-5d40f7cad7c871cf2ad2af19ac542994"); - m_FileReader = new UnityFileReader("archive:/CAB-5d40f7cad7c871cf2ad2af19ac542994", 1024*1024); + m_FileReader = new UnityFileReader("archive:/CAB-5d40f7cad7c871cf2ad2af19ac542994", 1024 * 1024); } - + [OneTimeTearDown] public void TearDown() { @@ -45,7 +48,7 @@ public void TearDown() UnityFileSystem.Cleanup(); } - + T ReadObject(long id, Func creator) { var objectInfo = m_SerializedFile.Objects.First(x => x.Id == id); @@ -60,7 +63,7 @@ public void TestTexture2d(string name, long id) { var texture = ReadObject(id, Texture2D.Read); var expectedTexture = (Texture2D)Context.ExpectedData.Get(name); - + Assert.AreEqual(expectedTexture.Name, texture.Name); Assert.AreEqual(expectedTexture.StreamDataSize, texture.StreamDataSize); Assert.AreEqual(expectedTexture.Width, texture.Width); @@ -69,24 +72,24 @@ public void TestTexture2d(string name, long id) Assert.AreEqual(expectedTexture.MipCount, texture.MipCount); Assert.AreEqual(expectedTexture.RwEnabled, texture.RwEnabled); } - + [Test] public void TestAnimationClip() { var clip = ReadObject(2152370074763270995, AnimationClip.Read); var expectedClip = (AnimationClip)Context.ExpectedData.Get("AnimationClip"); - + Assert.AreEqual(expectedClip.Name, clip.Name); Assert.AreEqual(expectedClip.Events, clip.Events); Assert.AreEqual(expectedClip.Legacy, clip.Legacy); } - + [Test] public void TestAudioClip() { var clip = ReadObject(-8074603400156879931, AudioClip.Read); var expectedClip = (AudioClip)Context.ExpectedData.Get("AudioClip"); - + Assert.AreEqual(expectedClip.Name, clip.Name); Assert.AreEqual(expectedClip.Channels, clip.Channels); Assert.AreEqual(expectedClip.Format, clip.Format); @@ -95,13 +98,13 @@ public void TestAudioClip() Assert.AreEqual(expectedClip.BitsPerSample, clip.BitsPerSample); Assert.AreEqual(expectedClip.StreamDataSize, clip.StreamDataSize); } - + [Test] public void TestAssetBundle() { var bundle = ReadObject(1, AssetBundle.Read); var expectedBundle = (AssetBundle)Context.ExpectedData.Get("AssetBundle"); - + Assert.AreEqual(expectedBundle.Name, bundle.Name); Assert.AreEqual(expectedBundle.Assets.Count, bundle.Assets.Count); @@ -109,19 +112,19 @@ public void TestAssetBundle() { var asset = bundle.Assets[i]; var expectedAsset = expectedBundle.Assets[i]; - + Assert.AreEqual(expectedAsset.Name, asset.Name); Assert.AreEqual(expectedAsset.PPtr.FileId, asset.PPtr.FileId); Assert.AreEqual(expectedAsset.PPtr.PathId, asset.PPtr.PathId); } } - + [Test] public void TestMesh() { var mesh = ReadObject(4693305862354978555, Mesh.Read); var expectedMesh = (Mesh)Context.ExpectedData.Get("Mesh"); - + Assert.AreEqual(expectedMesh.Name, mesh.Name); Assert.AreEqual(expectedMesh.Bones, mesh.Bones); Assert.AreEqual(expectedMesh.Compression, mesh.Compression); @@ -130,14 +133,14 @@ public void TestMesh() Assert.AreEqual(expectedMesh.BlendShapes, mesh.BlendShapes); Assert.AreEqual(expectedMesh.RwEnabled, mesh.RwEnabled); Assert.AreEqual(expectedMesh.StreamDataSize, mesh.StreamDataSize); - + Assert.AreEqual(expectedMesh.Channels.Count, mesh.Channels.Count); for (int i = 0; i < mesh.Channels.Count; ++i) { var channel = mesh.Channels[i]; var expectedChannel = expectedMesh.Channels[i]; - + Assert.AreEqual(expectedChannel.Dimension, channel.Dimension); Assert.AreEqual(expectedChannel.Type, channel.Type); Assert.AreEqual(expectedChannel.Usage, channel.Usage); @@ -149,7 +152,7 @@ public void TestShaderReader() { var shader = ReadObject(-4850512016903265157, Shader.Read); var expectedShader = (Shader)Context.ExpectedData.Get("Shader"); - + Assert.AreEqual(expectedShader.Name, shader.Name); Assert.AreEqual(expectedShader.DecompressedSize, shader.DecompressedSize); CollectionAssert.AreEquivalent(expectedShader.Keywords, shader.Keywords); @@ -159,14 +162,14 @@ public void TestShaderReader() { var subShader = shader.SubShaders[i]; var expectedSubShader = shader.SubShaders[i]; - + Assert.AreEqual(expectedSubShader.Passes.Count, subShader.Passes.Count); for (int j = 0; j < subShader.Passes.Count; ++j) { var pass = subShader.Passes[i]; var expectedPass = expectedSubShader.Passes[i]; - + Assert.AreEqual(expectedPass.Name, pass.Name); Assert.AreEqual(expectedPass.Programs.Count, pass.Programs.Count); CollectionAssert.AreEquivalent(expectedPass.Programs.Keys, pass.Programs.Keys); @@ -182,7 +185,7 @@ public void TestShaderReader() { var program = programs[k]; var expectedProgram = expectedPrograms[k]; - + Assert.AreEqual(expectedProgram.Api, program.Api); Assert.AreEqual(expectedProgram.BlobIndex, program.BlobIndex); Assert.AreEqual(expectedProgram.HwTier, program.HwTier); diff --git a/Analyzer/Analyzer.csproj b/Analyzer/Analyzer.csproj index df03d22..33b56cb 100644 --- a/Analyzer/Analyzer.csproj +++ b/Analyzer/Analyzer.csproj @@ -1,13 +1,27 @@ - + - net6.0 + net9.0 + latest + + + + AnyCPU + + + + AnyCPU - + + + + + + diff --git a/Analyzer/AnalyzerTool.cs b/Analyzer/AnalyzerTool.cs index 84217be..4fd5364 100644 --- a/Analyzer/AnalyzerTool.cs +++ b/Analyzer/AnalyzerTool.cs @@ -1,105 +1,113 @@ -using System; +using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; -using UnityDataTools.Analyzer.SQLite; -using UnityDataTools.FileSystem; +using UnityDataTools.Analyzer.SQLite.Handlers; +using UnityDataTools.Analyzer.SQLite.Parsers; +using UnityDataTools.Analyzer.SQLite.Parsers.Models; +using UnityDataTools.Analyzer.SQLite.Writers; namespace UnityDataTools.Analyzer; public class AnalyzerTool { - public int Analyze(string path, string databaseName, string searchPattern, bool skipReferences) + bool m_Verbose = false; + + public List parsers = new List() + { + new AddressablesBuildLayoutParser(), + new SerializedFileParser(), + }; + + public int Analyze( + string path, + string databaseName, + string searchPattern, + bool skipReferences, + bool verbose, + bool noRecursion) { - using SQLiteWriter writer = new (databaseName, skipReferences); - + m_Verbose = verbose; + + using SQLiteWriter writer = new(databaseName); + try { writer.Begin(); + foreach (var parser in parsers) + { + parser.Verbose = verbose; + parser.SkipReferences = skipReferences; + parser.Init(writer.Connection); + + } } catch (Exception e) { Console.Error.WriteLine($"Error creating database: {e.Message}"); return 1; } - + var timer = new Stopwatch(); timer.Start(); - var files = Directory.GetFiles(path, searchPattern, SearchOption.AllDirectories); + var files = Directory.GetFiles( + path, + searchPattern, + noRecursion ? SearchOption.TopDirectoryOnly : SearchOption.AllDirectories); + + int countFailures = 0; + int countSuccess = 0; + int countIgnored = 0; int i = 1; - int lastLength = 0; foreach (var file in files) { - try + bool foundParser = false; + foreach (var parser in parsers) { - UnityArchive archive = null; - - try - { - archive = UnityFileSystem.MountArchive(file, "archive:" + Path.DirectorySeparatorChar); - } - catch (NotSupportedException) - { - // It wasn't an AssetBundle, try to open the file as a SerializedFile. - - var relativePath = Path.GetRelativePath(path, file); - - Console.Write($"\rProcessing {i * 100 / files.Length}% ({i}/{files.Length}) {file}"); - - writer.WriteSerializedFile(relativePath, file, Path.GetDirectoryName(file)); - } - - if (archive != null) + if (parser.CanParse(file)) { + foundParser = true; try { - var assetBundleName = Path.GetRelativePath(path, file); - - writer.BeginAssetBundle(assetBundleName, new FileInfo(file).Length); - - var message = $"Processing {i * 100 / files.Length}% ({i}/{files.Length}) {assetBundleName}"; - Console.Write($"\r{message}{new string(' ', Math.Max(0, lastLength - message.Length))}"); - lastLength = message.Length; - - foreach (var node in archive.Nodes) - { - if (node.Flags.HasFlag(ArchiveNodeFlags.SerializedFile)) - { - try - { - writer.WriteSerializedFile(node.Path, "archive:/" + node.Path, Path.GetDirectoryName(file)); - } - catch (Exception e) - { - Console.Error.WriteLine(); - Console.Error.WriteLine($"Error processing {node.Path} in archive {file}"); - } - } - } + parser.Parse(file); + ReportProgress(Path.GetRelativePath(path, file), i, files.Length); + countSuccess++; } - finally + catch (Exception e) { - writer.EndAssetBundle(); - archive.Dispose(); + EraseProgressLine(); + Console.Error.WriteLine(); + Console.Error.WriteLine($"Error processing file: {file}"); + Console.WriteLine($"{e.GetType()}: {e.Message}"); + if (m_Verbose) + Console.WriteLine(e.StackTrace); + countFailures++; } } } - catch (Exception e) + if (!foundParser) { - Console.Error.WriteLine(); - Console.Error.WriteLine($"Error processing file {file}!"); - Console.Write($"{e.GetType()}: "); - Console.WriteLine(e.Message); - Console.WriteLine(e.StackTrace); - } + if (m_Verbose) + { + var relativePath = Path.GetRelativePath(path, file); + Console.WriteLine(); + Console.WriteLine($"Ignoring {relativePath}"); + } + countIgnored++; + } ++i; } Console.WriteLine(); - Console.WriteLine("Finalizing database..."); + Console.WriteLine($"Finalizing database. Successfully processed files: {countSuccess}, Failed files: {countFailures}, Ignored files: {countIgnored}"); writer.End(); + foreach (var parser in parsers) + { + parser.Dispose(); + } timer.Stop(); Console.WriteLine(); @@ -107,4 +115,31 @@ public int Analyze(string path, string databaseName, string searchPattern, bool return 0; } + + int m_LastProgressMessageLength = 0; + + void ReportProgress(string relativePath, int fileIndex, int cntFiles) + { + var message = $"Processing {fileIndex * 100 / cntFiles}% ({fileIndex}/{cntFiles}) {relativePath}"; + if (!m_Verbose) + { + EraseProgressLine(); + Console.Write($"\r{message}"); + } + else + { + Console.WriteLine(); + Console.WriteLine(message); + } + + m_LastProgressMessageLength = message.Length; + } + + void EraseProgressLine() + { + if (!m_Verbose) + Console.Write($"\r{new string(' ', m_LastProgressMessageLength)}\r"); + else + Console.WriteLine(); + } } diff --git a/Analyzer/IWriter.cs b/Analyzer/IWriter.cs deleted file mode 100644 index 7700c08..0000000 --- a/Analyzer/IWriter.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityDataTools.Analyzer.SerializedObjects; -using UnityDataTools.FileSystem; - -namespace UnityDataTools.Analyzer; - -public interface IWriter : IDisposable -{ - void Begin(); - void BeginAssetBundle(string name, long size); - void EndAssetBundle(); - void WriteSerializedFile(string relativePath, string fullPath, string containingFolder); - void End(); -} diff --git a/Analyzer/PPtrAndCrcProcessor.cs b/Analyzer/PPtrAndCrcProcessor.cs index 40b2d77..a9d5b13 100644 --- a/Analyzer/PPtrAndCrcProcessor.cs +++ b/Analyzer/PPtrAndCrcProcessor.cs @@ -1,9 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Text; -using UnityDataTools.FileSystem; using Force.Crc32; +using UnityDataTools.FileSystem; namespace UnityDataTools.Analyzer; @@ -12,7 +12,7 @@ namespace UnityDataTools.Analyzer; public class PPtrAndCrcProcessor : IDisposable { public delegate int CallbackDelegate(long objectId, int fileId, long pathId, string propertyPath, string propertyType); - + private SerializedFile m_SerializedFile; private UnityFileReader m_Reader; private long m_Offset; @@ -21,7 +21,7 @@ public class PPtrAndCrcProcessor : IDisposable private string m_Folder; private StringBuilder m_StringBuilder = new(); private byte[] m_pptrBytes = new byte[4]; - + private CallbackDelegate m_Callback; private Dictionary m_resourceReaders = new(); @@ -34,14 +34,14 @@ public PPtrAndCrcProcessor(SerializedFile serializedFile, UnityFileReader reader m_Folder = folder; m_Callback = callback; } - + public void Dispose() { foreach (var r in m_resourceReaders.Values) { r?.Dispose(); } - + m_resourceReaders.Clear(); } @@ -52,7 +52,7 @@ private UnityFileReader GetResourceReader(string filename) { filename = filename.Remove(0, slashPos + 1); } - + if (!m_resourceReaders.TryGetValue(filename, out var reader)) { try @@ -65,7 +65,7 @@ private UnityFileReader GetResourceReader(string filename) { reader = new UnityFileReader(Path.Join(m_Folder, filename), 4 * 1024 * 1024); } - catch (Exception e) + catch (Exception) { Console.Error.WriteLine(); Console.Error.WriteLine($"Error opening resource file {filename}"); @@ -89,13 +89,13 @@ public uint Process(long objectId, long offset, TypeTreeNode node) { m_StringBuilder.Clear(); m_StringBuilder.Append(child.Name); - ProcessNode(child); + ProcessNode(child, false); } return m_Crc32; } - private void ProcessNode(TypeTreeNode node) + private void ProcessNode(TypeTreeNode node, bool isInManagedReferenceRegistry) { if (node.IsBasicType) { @@ -104,18 +104,18 @@ private void ProcessNode(TypeTreeNode node) } else if (node.IsArray) { - ProcessArray(node); + ProcessArray(node, false, isInManagedReferenceRegistry); } else if (node.Type == "vector" || node.Type == "map" || node.Type == "staticvector") { - ProcessArray(node.Children[0]); + ProcessArray(node.Children[0], false, isInManagedReferenceRegistry); } else if (node.Type.StartsWith("PPtr<")) { var startIndex = node.Type.IndexOf('<') + 1; var endIndex = node.Type.Length - 1; var referencedType = node.Type.Substring(startIndex, endIndex - startIndex); - + ExtractPPtr(referencedType); } else if (node.Type == "StreamingInfo") @@ -125,10 +125,10 @@ private void ProcessNode(TypeTreeNode node) var offset = node.Children[0].Size == 4 ? m_Reader.ReadInt32(m_Offset) : m_Reader.ReadInt64(m_Offset); m_Offset += node.Children[0].Size; - + var size = m_Reader.ReadInt32(m_Offset); m_Offset += 4; - + var stringSize = m_Reader.ReadInt32(m_Offset); var filename = m_Reader.ReadString(m_Offset + 4, stringSize); m_Offset += stringSize + 4; @@ -148,7 +148,7 @@ private void ProcessNode(TypeTreeNode node) { if (node.Children.Count != 3) throw new Exception("Invalid StreamedResource"); - + var stringSize = m_Reader.ReadInt32(m_Offset); var filename = m_Reader.ReadString(m_Offset + 4, stringSize); m_Offset += stringSize + 4; @@ -156,7 +156,7 @@ private void ProcessNode(TypeTreeNode node) var offset = m_Reader.ReadInt64(m_Offset); m_Offset += 8; - + var size = (int)m_Reader.ReadInt64(m_Offset); m_Offset += 8; @@ -178,7 +178,9 @@ private void ProcessNode(TypeTreeNode node) } else if (node.IsManagedReferenceRegistry) { - ProcessManagedReferenceRegistry(node); + // ManagedReferenceRegistry are never nested + if (!isInManagedReferenceRegistry) + ProcessManagedReferenceRegistry(node); } else { @@ -187,11 +189,11 @@ private void ProcessNode(TypeTreeNode node) var size = m_StringBuilder.Length; m_StringBuilder.Append('.'); m_StringBuilder.Append(child.Name); - ProcessNode(child); + ProcessNode(child, isInManagedReferenceRegistry); m_StringBuilder.Remove(size, m_StringBuilder.Length - size); } } - + if ( ((int)node.MetaFlags & (int)TypeTreeMetaFlags.AlignBytes) != 0 || ((int)node.MetaFlags & (int)TypeTreeMetaFlags.AnyChildUsesAlignBytes) != 0 @@ -201,7 +203,7 @@ private void ProcessNode(TypeTreeNode node) } } - private void ProcessArray(TypeTreeNode node, bool isManagedReferenceRegistry = false) + private void ProcessArray(TypeTreeNode node, bool isManagedReferenceRegistry, bool isInManagedReferenceRegistry) { var dataNode = node.Children[1]; @@ -221,14 +223,13 @@ private void ProcessArray(TypeTreeNode node, bool isManagedReferenceRegistry = f { if (!isManagedReferenceRegistry) { - var size = m_StringBuilder.Length; m_StringBuilder.Append('['); m_StringBuilder.Append(i); m_StringBuilder.Append(']'); - - ProcessNode(dataNode); - + + ProcessNode(dataNode, isInManagedReferenceRegistry); + m_StringBuilder.Remove(size, m_StringBuilder.Length - size); } else @@ -285,12 +286,12 @@ private void ProcessManagedReferenceRegistry(TypeTreeNode node) var size = m_StringBuilder.Length; m_StringBuilder.Append('.'); m_StringBuilder.Append("RefIds"); - ProcessArray(refIdsArrayNode, true); + ProcessArray(refIdsArrayNode, true, true); m_StringBuilder.Remove(size, m_StringBuilder.Length - size); } else { - throw new Exception("Unsupported ManagedReferenceRegistry version"); + throw new Exception($"Unsupported ManagedReferenceRegistry version {version}"); } } @@ -330,9 +331,9 @@ bool ProcessManagedReferenceData(TypeTreeNode refTypeNode, TypeTreeNode referenc m_StringBuilder.Append("rid("); m_StringBuilder.Append(rid); m_StringBuilder.Append(").data"); - ProcessNode(refTypeTypeTree); + ProcessNode(refTypeTypeTree, true); m_StringBuilder.Remove(size, m_StringBuilder.Length - size); - + return true; } @@ -353,4 +354,4 @@ private void ExtractPPtr(string referencedType) m_Crc32 = Crc32Algorithm.Append(m_Crc32, m_pptrBytes); } } -} \ No newline at end of file +} diff --git a/Analyzer/Properties/Resources.Designer.cs b/Analyzer/Properties/Resources.Designer.cs index 487a20a..6eab80f 100644 --- a/Analyzer/Properties/Resources.Designer.cs +++ b/Analyzer/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace UnityDataTools.Analyzer.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { @@ -60,6 +60,487 @@ internal Resources() { } } + /// + /// Looks up a localized string similar to create table addressables_build_bundle_dependencies + ///( + /// bundle_id INTEGER, + /// build_id INTEGER, + /// dependency_rid INTEGER, + /// PRIMARY KEY (bundle_id, build_id, dependency_rid), + /// FOREIGN KEY (bundle_id, build_id) REFERENCES addressables_build_bundles(id, build_id) + ///);. + /// + internal static string AddrBuildBundleDependencies { + get { + return ResourceManager.GetString("AddrBuildBundleDependencies", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to create table addressables_build_bundle_dependent_bundles + ///( + /// bundle_id INTEGER, + /// build_id INTEGER, + /// dependent_bundle_rid INTEGER, + /// PRIMARY KEY (bundle_id, build_id, dependent_bundle_rid), + /// FOREIGN KEY (bundle_id, build_id) REFERENCES addressables_build_bundles(id, build_id) + ///);. + /// + internal static string AddrBuildBundleDependentBundles { + get { + return ResourceManager.GetString("AddrBuildBundleDependentBundles", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to create table addressables_build_bundle_expanded_dependencies + ///( + /// bundle_id INTEGER, + /// build_id INTEGER, + /// dependency_rid INTEGER, + /// PRIMARY KEY (bundle_id, build_id, dependency_rid), + /// FOREIGN KEY (bundle_id, build_id) REFERENCES addressables_build_bundles(id, build_id) + ///);. + /// + internal static string AddrBuildBundleExpandedDependencies { + get { + return ResourceManager.GetString("AddrBuildBundleExpandedDependencies", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to create table addressables_build_bundle_files + ///( + /// bundle_id INTEGER, + /// build_id INTEGER, + /// file_rid INTEGER, + /// PRIMARY KEY (bundle_id, build_id, file_rid), + /// FOREIGN KEY (bundle_id, build_id) REFERENCES addressables_build_bundles(id, build_id) + ///);. + /// + internal static string AddrBuildBundleFiles { + get { + return ResourceManager.GetString("AddrBuildBundleFiles", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to create table addressables_build_bundle_regular_dependencies + ///( + /// bundle_id INTEGER, + /// build_id INTEGER, + /// dependency_rid INTEGER, + /// PRIMARY KEY (bundle_id, build_id, dependency_rid), + /// FOREIGN KEY (bundle_id, build_id) REFERENCES addressables_build_bundles(id, build_id) + ///);. + /// + internal static string AddrBuildBundleRegularDependencies { + get { + return ResourceManager.GetString("AddrBuildBundleRegularDependencies", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to create table addressables_build_bundles + ///( + /// id INTEGER, + /// build_id INTEGER, + /// asset_count INTEGER, + /// build_status INTEGER, + /// crc INTEGER, + /// compression TEXT, + /// dependency_file_size INTEGER, + /// expanded_dependency_file_size INTEGER, + /// file_size INTEGER, + /// group_rid INTEGER, + /// hash TEXT, + /// internal_name TEXT, + /// load_path TEXT, + /// name TEXT, + /// provider TEXT, + /// result_type TEXT, + /// PRIMARY KEY (id, build_id) + ///);. + /// + internal static string AddrBuildBundles { + get { + return ResourceManager.GetString("AddrBuildBundles", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to create table addressables_build_data_from_other_asset_object_references + ///( + /// data_from_other_asset_id INTEGER, + /// build_id INTEGER, + /// local_identifier_in_file INTEGER, + /// asset_id INTEGER, + /// object_id INTEGER, + /// PRIMARY KEY (data_from_other_asset_id, build_id, local_identifier_in_file, asset_id, object_id), + /// FOREIGN KEY (data_from_other_asset_id, build_id, local_identifier_in_file) REFERENCES addressables_build_data_from_other_asset_objects(data_from_other_asset_id, build_id, local_identifier_in_file) + /// [rest of string was truncated]";. + /// + internal static string AddrBuildDataFromOtherAssetObjectReferences { + get { + return ResourceManager.GetString("AddrBuildDataFromOtherAssetObjectReferences", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to create table addressables_build_data_from_other_asset_objects + ///( + /// data_from_other_asset_id INTEGER, + /// build_id INTEGER, + /// asset_type INTEGER, + /// component_name TEXT, + /// local_identifier_in_file INTEGER, + /// object_name TEXT, + /// serialized_size INTEGER, + /// streamed_size INTEGER, + /// PRIMARY KEY (data_from_other_asset_id, build_id, local_identifier_in_file), + /// FOREIGN KEY (data_from_other_asset_id, build_id) REFERENCES addressables_build_data_from_other_assets(id, build_id) + ///);. + /// + internal static string AddrBuildDataFromOtherAssetObjects { + get { + return ResourceManager.GetString("AddrBuildDataFromOtherAssetObjects", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to create table addressables_build_data_from_other_asset_referencing_assets + ///( + /// data_from_other_asset_id INTEGER, + /// build_id INTEGER, + /// referencing_asset_rid INTEGER, + /// PRIMARY KEY (data_from_other_asset_id, build_id, referencing_asset_rid), + /// FOREIGN KEY (data_from_other_asset_id, build_id) REFERENCES addressables_build_data_from_other_assets(id, build_id) + ///);. + /// + internal static string AddrBuildDataFromOtherAssetReferencingAssets { + get { + return ResourceManager.GetString("AddrBuildDataFromOtherAssetReferencingAssets", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to create table addressables_build_data_from_other_assets + ///( + /// id INTEGER, + /// build_id INTEGER, + /// asset_guid TEXT, + /// asset_path TEXT, + /// file INTEGER, + /// main_asset_type INTEGER, + /// object_count INTEGER, + /// serialized_size INTEGER, + /// streamed_size INTEGER, + /// PRIMARY KEY (id, build_id) + ///);. + /// + internal static string AddrBuildDataFromOtherAssets { + get { + return ResourceManager.GetString("AddrBuildDataFromOtherAssets", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to create table addressables_build_explicit_asset_externally_referenced_assets + ///( + /// explicit_asset_id INTEGER, + /// build_id INTEGER, + /// externally_referenced_asset_rid INTEGER, + /// PRIMARY KEY (explicit_asset_id, build_id, externally_referenced_asset_rid), + /// FOREIGN KEY (explicit_asset_id, build_id) REFERENCES addressables_build_explicit_assets(id, build_id) + ///);. + /// + internal static string AddrBuildExplicitAssetExternallyReferencedAssets { + get { + return ResourceManager.GetString("AddrBuildExplicitAssetExternallyReferencedAssets", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to create table addressables_build_explicit_asset_internal_referenced_explicit_assets + ///( + /// explicit_asset_id INTEGER, + /// build_id INTEGER, + /// internal_referenced_explicit_asset_rid INTEGER, + /// PRIMARY KEY (explicit_asset_id, build_id, internal_referenced_explicit_asset_rid), + /// FOREIGN KEY (explicit_asset_id, build_id) REFERENCES addressables_build_explicit_assets(id, build_id) + ///);. + /// + internal static string AddrBuildExplicitAssetInternalReferencedExplicitAssets { + get { + return ResourceManager.GetString("AddrBuildExplicitAssetInternalReferencedExplicitAssets", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to create table addressables_build_explicit_asset_internal_referenced_other_assets + ///( + /// explicit_asset_id INTEGER, + /// build_id INTEGER, + /// internal_referenced_other_asset_rid INTEGER, + /// PRIMARY KEY (explicit_asset_id, build_id, internal_referenced_other_asset_rid), + /// FOREIGN KEY (explicit_asset_id, build_id) REFERENCES addressables_build_explicit_assets(id, build_id) + ///);. + /// + internal static string AddrBuildExplicitAssetInternalReferencedOtherAssets { + get { + return ResourceManager.GetString("AddrBuildExplicitAssetInternalReferencedOtherAssets", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to create table addressables_build_explicit_asset_labels + ///( + /// explicit_asset_id INTEGER, + /// build_id INTEGER, + /// label TEXT, + /// PRIMARY KEY (explicit_asset_id, build_id, label), + /// FOREIGN KEY (explicit_asset_id, build_id) REFERENCES addressables_build_explicit_assets(id, build_id) + ///);. + /// + internal static string AddrBuildExplicitAssetLabels { + get { + return ResourceManager.GetString("AddrBuildExplicitAssetLabels", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to create table addressables_build_explicit_assets + ///( + /// id INTEGER, + /// build_id INTEGER, + /// bundle INTEGER, + /// file INTEGER, + /// asset_hash TEXT, + /// asset_path TEXT, + /// addressable_name TEXT, + /// group_guid TEXT, + /// guid TEXT, + /// internal_id TEXT, + /// main_asset_type INTEGER, + /// serialized_size INTEGER, + /// streamed_size INTEGER, + /// PRIMARY KEY (id, build_id) + ///);. + /// + internal static string AddrBuildExplicitAssets { + get { + return ResourceManager.GetString("AddrBuildExplicitAssets", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to create table addressables_build_file_assets + ///( + /// file_id INTEGER, + /// build_id INTEGER, + /// asset_rid INTEGER, + /// PRIMARY KEY (file_id, build_id, asset_rid), + /// FOREIGN KEY (file_id, build_id) REFERENCES addressables_build_files(id, build_id) + ///);. + /// + internal static string AddrBuildFileAssets { + get { + return ResourceManager.GetString("AddrBuildFileAssets", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to create table addressables_build_file_external_references + ///( + /// file_id INTEGER, + /// build_id INTEGER, + /// external_reference_rid INTEGER, + /// PRIMARY KEY (file_id, build_id, external_reference_rid), + /// FOREIGN KEY (file_id, build_id) REFERENCES addressables_build_files(id, build_id) + ///);. + /// + internal static string AddrBuildFileExternalReferences { + get { + return ResourceManager.GetString("AddrBuildFileExternalReferences", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to create table addressables_build_file_other_assets + ///( + /// file_id INTEGER, + /// build_id INTEGER, + /// other_asset_rid INTEGER, + /// PRIMARY KEY (file_id, build_id, other_asset_rid), + /// FOREIGN KEY (file_id, build_id) REFERENCES addressables_build_files(id, build_id) + ///);. + /// + internal static string AddrBuildFileOtherAssets { + get { + return ResourceManager.GetString("AddrBuildFileOtherAssets", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to create table addressables_build_files + ///( + /// id INTEGER, + /// build_id INTEGER, + /// bundle INTEGER, + /// bundle_object_info_size INTEGER, + /// mono_script_count INTEGER, + /// mono_script_size INTEGER, + /// name TEXT, + /// preload_info_size INTEGER, + /// write_result_filename TEXT, + /// PRIMARY KEY (id, build_id) + ///);. + /// + internal static string AddrBuildFiles { + get { + return ResourceManager.GetString("AddrBuildFiles", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to create table addressables_build_file_sub_files + ///( + /// file_id INTEGER, + /// build_id INTEGER, + /// sub_file_rid INTEGER, + /// PRIMARY KEY (file_id, build_id, sub_file_rid), + /// FOREIGN KEY (file_id, build_id) REFERENCES addressables_build_files(id, build_id) + ///);. + /// + internal static string AddrBuildFileSubFiles { + get { + return ResourceManager.GetString("AddrBuildFileSubFiles", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to create table addressables_build_group_bundles + ///( + /// group_id INTEGER, + /// build_id INTEGER, + /// bundle_rid INTEGER, + /// PRIMARY KEY (group_id, build_id, bundle_rid), + /// FOREIGN KEY (group_id, build_id) REFERENCES addressables_build_groups(id, build_id) + ///);. + /// + internal static string AddrBuildGroupBundles { + get { + return ResourceManager.GetString("AddrBuildGroupBundles", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to create table addressables_build_groups + ///( + /// id INTEGER, + /// build_id INTEGER, + /// guid TEXT, + /// name TEXT, + /// packing_mode TEXT, + /// PRIMARY KEY (id, build_id) + ///);. + /// + internal static string AddrBuildGroups { + get { + return ResourceManager.GetString("AddrBuildGroups", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to create table addressables_build_group_schemas + ///( + /// group_id INTEGER, + /// build_id INTEGER, + /// schema_rid INTEGER, + /// PRIMARY KEY (group_id, build_id, schema_rid), + /// FOREIGN KEY (group_id, build_id) REFERENCES addressables_build_groups(id, build_id) + ///);. + /// + internal static string AddrBuildGroupSchemas { + get { + return ResourceManager.GetString("AddrBuildGroupSchemas", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to CREATE TABLE addressables_builds + ///( + /// id INTEGER, + /// name TEXT, + /// build_target INTEGER, + /// start_time TEXT, + /// duration REAL, + /// error TEXT, + /// package_version TEXT, + /// player_version TEXT, + /// build_script TEXT, + /// result_hash TEXT, + /// type INTEGER, + /// unity_version TEXT, + /// PRIMARY KEY (id) + ///);. + /// + internal static string AddrBuilds { + get { + return ResourceManager.GetString("AddrBuilds", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to create table addressables_build_schema_data_pairs + ///( + /// schema_id INTEGER, + /// build_id INTEGER, + /// key TEXT, + /// value TEXT, + /// PRIMARY KEY (schema_id, build_id, key), + /// FOREIGN KEY (schema_id, build_id) REFERENCES addressables_build_schemas(id, build_id) + ///);. + /// + internal static string AddrBuildSchemaDataPairs { + get { + return ResourceManager.GetString("AddrBuildSchemaDataPairs", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to create table addressables_build_schemas + ///( + /// id INTEGER, + /// build_id INTEGER, + /// guid TEXT, + /// type TEXT, + /// PRIMARY KEY (id, build_id) + ///);. + /// + internal static string AddrBuildSchemas { + get { + return ResourceManager.GetString("AddrBuildSchemas", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to create table addressables_build_sub_files + ///( + /// id INTEGER, + /// build_id INTEGER, + /// is_serialized_file INTEGER, + /// name TEXT, + /// size INTEGER, + /// PRIMARY KEY (id, build_id) + ///);. + /// + internal static string AddrBuildSubFiles { + get { + return ResourceManager.GetString("AddrBuildSubFiles", resourceCulture); + } + } + /// /// Looks up a localized string similar to CREATE TABLE animation_clips( /// id INTEGER, @@ -88,12 +569,21 @@ internal static string AnimationClip { /// name TEXT ///); /// + ///CREATE TABLE asset_dependencies( + /// object INTEGER, + /// dependency INTEGER + ///); + /// ///CREATE VIEW asset_view AS ///SELECT /// a.name AS asset_name, /// o.* ///FROM assets a INNER JOIN object_view o ON o.id = a.object; - ///. + /// + ///CREATE VIEW asset_dependencies_view AS + ///SELECT a.id, a.asset_name, a.asset_bundle, a.type, od.id dep_id, od.asset_bundle dep_asset_bundle, od.name dep_name, od.type dep_type + ///FROM asset_view a + ///INNER JOIN asset_dependencies d ON a [rest of string was truncated]";. /// internal static string AssetBundle { get { @@ -138,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); @@ -183,7 +733,7 @@ internal static string Finalize { /// name TEXT, /// game_object INTEGER, /// size INTEGER, - /// PRIMARY KE [rest of string was truncated]";. + /// crc32 INTE [rest of string was truncated]";. /// internal static string Init { get { @@ -201,6 +751,8 @@ internal static string Init { /// vertices INTEGER, /// compression INTEGER, /// rw_enabled INTEGER, + /// vertex_size INTEGER, + /// channels TEXT, /// PRIMARY KEY (id) ///); /// @@ -213,10 +765,11 @@ internal static string Init { /// m.indices, /// m.vertices, /// m.compression, - /// m.rw_enabled + /// m.rw_enabled, + /// m.vertex_size, + /// m.channels ///FROM meshes m - ///INNER JOIN object_view o ON o.id = m.id; - ///. + ///INNER JOIN [rest of string was truncated]";. /// internal static string Mesh { get { @@ -228,12 +781,21 @@ internal static string Mesh { /// Looks up a localized string similar to CREATE TABLE shaders( /// id INTEGER, /// decompressed_size INTEGER, - /// sub_shaders INTEGER, /// unique_programs INTEGER, - /// keywords TEXT, /// PRIMARY KEY (id) ///); /// + ///CREATE TABLE shader_keywords( + /// id INTEGER, + /// keyword TEXT, + /// PRIMARY KEY (id) + ///); + /// + ///CREATE TABLE shader_subprogram_keywords( + /// subprogram_id INTEGER, + /// keyword_id INTEGER + ///); + /// ///CREATE TABLE shader_apis( /// id INTEGER, /// name TEXT, @@ -241,19 +803,10 @@ internal static string Mesh { ///); /// ///CREATE TABLE shader_subprograms( + /// id INTEGER, /// shader INTEGER, - /// pass INTEGER, - /// sub_program INTEGER, - /// hw_tier INTEGER, - /// shader_type TEXT, - /// api INTEGER, - /// keywords TEXT - ///); - /// - ///CREATE VIEW shader_view AS - ///SELECT - /// o.*, - /// s.decompre [rest of string was truncated]";. + /// sub_shader INTEGER, + /// [rest of string was truncated]";. /// internal static string Shader { get { @@ -297,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 1144559..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 @@ -142,4 +145,91 @@ ..\Resources\Texture2D.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 - \ No newline at end of file + + ..\Resources\AddrBuilds.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Resources\AddrBuildBundles.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Resources\AddrBuildBundleDependentBundles.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Resources\AddrBuildBundleDependencies.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Resources\AddrBuildBundleExpandedDependencies.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Resources\AddrBuildBundleFiles.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Resources\AddrBuildBundleRegularDependencies.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Resources\AddrBuildDataFromOtherAssets.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Resources\AddrBuildDataFromOtherAssetObjects.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Resources\AddrBuildDataFromOtherAssetObjectReferences.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Resources\AddrBuildDataFromOtherAssetReferencingAssets.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Resources\AddrBuildExplicitAssets.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Resources\AddrBuildExplicitAssetExternallyReferencedAssets.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Resources\AddrBuildExplicitAssetInternalReferencedExplicitAssets.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Resources\AddrBuildExplicitAssetInternalReferencedOtherAssets.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Resources\AddrBuildExplicitAssetLabels.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Resources\AddrBuildFiles.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Resources\AddrBuildFileAssets.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Resources\AddrBuildFileOtherAssets.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Resources\AddrBuildFileSubFiles.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Resources\AddrBuildFileExternalReferences.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Resources\AddrBuildGroupBundles.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Resources\AddrBuildGroups.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Resources\AddrBuildGroupSchemas.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Resources\AddrBuildSchemas.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Resources\AddrBuildSubFiles.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\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 29c306e..5d4c2b9 100644 --- a/Analyzer/README.md +++ b/Analyzer/README.md @@ -1,180 +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 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. - -It is possible to extract type-specific properties using classes inheriting from the -[SerializedObject](./SerializedObjects/SerializedObject.cs) class. The project already provides -the most common SerializedObject implementations in the [SerializedObjects](./SerializedObjects) -folder. - -# How to use the library - -The [AnalyzerTool](./AnalyzerTool.cs) class is the API entry point. The main method is called -Analyze. It is currently hardcoded to write using the [SQLiteWriter](./SQLite/SQLiteWriter.cs), -but it can easily be adapter to use another writer type. It takes four parameters: -* path (string): path of the folder where the files to analyze are located. It will be searched - recursively. -* databaseName (string): database filename, it will be overwritten if it already exists. -* searchPattern (string): file search pattern (e.g. \*.bundle). -* skipReferences (bool): determines if the CRC calculation and references (PPtrs) extraction must - skipped. This is faster, but the refs table will be empty and the duplicate assets won't be - accurate. -Calling this method will create the SQLite output database and 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). - -# How to use the database - -A tool such as the [DB Browser for SQLite](https://sqlitebrowser.org/) is required to look at the -content of the database. The database provides different views that can be used to easily find the -information you might need. - -## 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. +See [Documentation/analyzer.md](../Documentation/analyzer.md) diff --git a/Analyzer/Resources/AddrBuildBundleDependencies.sql b/Analyzer/Resources/AddrBuildBundleDependencies.sql new file mode 100644 index 0000000..ed3caa6 --- /dev/null +++ b/Analyzer/Resources/AddrBuildBundleDependencies.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS addressables_build_bundle_dependencies +( + bundle_id INTEGER, + build_id INTEGER, + dependency_rid INTEGER, + PRIMARY KEY (bundle_id, build_id, dependency_rid), + FOREIGN KEY (bundle_id, build_id) REFERENCES addressables_build_bundles(id, build_id) +); \ No newline at end of file diff --git a/Analyzer/Resources/AddrBuildBundleDependentBundles.sql b/Analyzer/Resources/AddrBuildBundleDependentBundles.sql new file mode 100644 index 0000000..eb36fbf --- /dev/null +++ b/Analyzer/Resources/AddrBuildBundleDependentBundles.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS addressables_build_bundle_dependent_bundles +( + bundle_id INTEGER, + build_id INTEGER, + dependent_bundle_rid INTEGER, + PRIMARY KEY (bundle_id, build_id, dependent_bundle_rid), + FOREIGN KEY (bundle_id, build_id) REFERENCES addressables_build_bundles(id, build_id) +); \ No newline at end of file diff --git a/Analyzer/Resources/AddrBuildBundleExpandedDependencies.sql b/Analyzer/Resources/AddrBuildBundleExpandedDependencies.sql new file mode 100644 index 0000000..6f97a83 --- /dev/null +++ b/Analyzer/Resources/AddrBuildBundleExpandedDependencies.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS addressables_build_bundle_expanded_dependencies +( + bundle_id INTEGER, + build_id INTEGER, + dependency_rid INTEGER, + PRIMARY KEY (bundle_id, build_id, dependency_rid), + FOREIGN KEY (bundle_id, build_id) REFERENCES addressables_build_bundles(id, build_id) +); \ No newline at end of file diff --git a/Analyzer/Resources/AddrBuildBundleFiles.sql b/Analyzer/Resources/AddrBuildBundleFiles.sql new file mode 100644 index 0000000..ac3f42a --- /dev/null +++ b/Analyzer/Resources/AddrBuildBundleFiles.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS addressables_build_bundle_files +( + bundle_id INTEGER, + build_id INTEGER, + file_rid INTEGER, + PRIMARY KEY (bundle_id, build_id, file_rid), + FOREIGN KEY (bundle_id, build_id) REFERENCES addressables_build_bundles(id, build_id) +); \ No newline at end of file diff --git a/Analyzer/Resources/AddrBuildBundleRegularDependencies.sql b/Analyzer/Resources/AddrBuildBundleRegularDependencies.sql new file mode 100644 index 0000000..f9dab18 --- /dev/null +++ b/Analyzer/Resources/AddrBuildBundleRegularDependencies.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS addressables_build_bundle_regular_dependencies +( + bundle_id INTEGER, + build_id INTEGER, + dependency_rid INTEGER, + PRIMARY KEY (bundle_id, build_id, dependency_rid), + FOREIGN KEY (bundle_id, build_id) REFERENCES addressables_build_bundles(id, build_id) +); \ No newline at end of file diff --git a/Analyzer/Resources/AddrBuildBundles.sql b/Analyzer/Resources/AddrBuildBundles.sql new file mode 100644 index 0000000..80cdc86 --- /dev/null +++ b/Analyzer/Resources/AddrBuildBundles.sql @@ -0,0 +1,22 @@ +CREATE TABLE IF NOT EXISTS addressables_build_bundles +( + id INTEGER, + build_id INTEGER, + asset_count INTEGER, + build_status INTEGER, + crc INTEGER, + compression TEXT, + dependency_file_size INTEGER, + expanded_dependency_file_size INTEGER, + file_size INTEGER, + group_rid INTEGER, + hash TEXT, + internal_name TEXT, + load_path TEXT, + name TEXT, + provider TEXT, + result_type TEXT, + PRIMARY KEY (id, build_id) +); + +CREATE VIEW IF NOT EXISTS addressables_build_cached_bundles AS SELECT build_id, concat(internal_name, '.bundle') AS cached_name, name AS name_in_catalog FROM addressables_build_bundles; diff --git a/Analyzer/Resources/AddrBuildDataFromOtherAssetObjectReferences.sql b/Analyzer/Resources/AddrBuildDataFromOtherAssetObjectReferences.sql new file mode 100644 index 0000000..cb624cb --- /dev/null +++ b/Analyzer/Resources/AddrBuildDataFromOtherAssetObjectReferences.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS addressables_build_data_from_other_asset_object_references +( + data_from_other_asset_id INTEGER, + build_id INTEGER, + local_identifier_in_file INTEGER, + asset_id INTEGER, + object_id INTEGER, + PRIMARY KEY (data_from_other_asset_id, build_id, local_identifier_in_file, asset_id, object_id), + FOREIGN KEY (data_from_other_asset_id, build_id, local_identifier_in_file) REFERENCES addressables_build_data_from_other_asset_objects(data_from_other_asset_id, build_id, local_identifier_in_file) +); \ No newline at end of file diff --git a/Analyzer/Resources/AddrBuildDataFromOtherAssetObjects.sql b/Analyzer/Resources/AddrBuildDataFromOtherAssetObjects.sql new file mode 100644 index 0000000..5ce55b1 --- /dev/null +++ b/Analyzer/Resources/AddrBuildDataFromOtherAssetObjects.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS addressables_build_data_from_other_asset_objects +( + data_from_other_asset_id INTEGER, + build_id INTEGER, + asset_type INTEGER, + component_name TEXT, + local_identifier_in_file INTEGER, + object_name TEXT, + serialized_size INTEGER, + streamed_size INTEGER, + PRIMARY KEY (data_from_other_asset_id, build_id, local_identifier_in_file), + FOREIGN KEY (data_from_other_asset_id, build_id) REFERENCES addressables_build_data_from_other_assets(id, build_id) +); \ No newline at end of file diff --git a/Analyzer/Resources/AddrBuildDataFromOtherAssetReferencingAssets.sql b/Analyzer/Resources/AddrBuildDataFromOtherAssetReferencingAssets.sql new file mode 100644 index 0000000..a6a1dc3 --- /dev/null +++ b/Analyzer/Resources/AddrBuildDataFromOtherAssetReferencingAssets.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS addressables_build_data_from_other_asset_referencing_assets +( + data_from_other_asset_id INTEGER, + build_id INTEGER, + referencing_asset_rid INTEGER, + PRIMARY KEY (data_from_other_asset_id, build_id, referencing_asset_rid), + FOREIGN KEY (data_from_other_asset_id, build_id) REFERENCES addressables_build_data_from_other_assets(id, build_id) +); \ No newline at end of file diff --git a/Analyzer/Resources/AddrBuildDataFromOtherAssets.sql b/Analyzer/Resources/AddrBuildDataFromOtherAssets.sql new file mode 100644 index 0000000..426fd19 --- /dev/null +++ b/Analyzer/Resources/AddrBuildDataFromOtherAssets.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS addressables_build_data_from_other_assets +( + id INTEGER, + build_id INTEGER, + asset_guid TEXT, + asset_path TEXT, + file INTEGER, + main_asset_type INTEGER, + object_count INTEGER, + serialized_size INTEGER, + streamed_size INTEGER, + PRIMARY KEY (id, build_id) +); \ No newline at end of file diff --git a/Analyzer/Resources/AddrBuildExplicitAssetExternallyReferencedAssets.sql b/Analyzer/Resources/AddrBuildExplicitAssetExternallyReferencedAssets.sql new file mode 100644 index 0000000..da57425 --- /dev/null +++ b/Analyzer/Resources/AddrBuildExplicitAssetExternallyReferencedAssets.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS addressables_build_explicit_asset_externally_referenced_assets +( + explicit_asset_id INTEGER, + build_id INTEGER, + externally_referenced_asset_rid INTEGER, + PRIMARY KEY (explicit_asset_id, build_id, externally_referenced_asset_rid), + FOREIGN KEY (explicit_asset_id, build_id) REFERENCES addressables_build_explicit_assets(id, build_id) +); \ No newline at end of file diff --git a/Analyzer/Resources/AddrBuildExplicitAssetInternalReferencedExplicitAssets.sql b/Analyzer/Resources/AddrBuildExplicitAssetInternalReferencedExplicitAssets.sql new file mode 100644 index 0000000..f0671ad --- /dev/null +++ b/Analyzer/Resources/AddrBuildExplicitAssetInternalReferencedExplicitAssets.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS addressables_build_explicit_asset_internal_referenced_explicit_assets +( + explicit_asset_id INTEGER, + build_id INTEGER, + internal_referenced_explicit_asset_rid INTEGER, + PRIMARY KEY (explicit_asset_id, build_id, internal_referenced_explicit_asset_rid), + FOREIGN KEY (explicit_asset_id, build_id) REFERENCES addressables_build_explicit_assets(id, build_id) +); \ No newline at end of file diff --git a/Analyzer/Resources/AddrBuildExplicitAssetInternalReferencedOtherAssets.sql b/Analyzer/Resources/AddrBuildExplicitAssetInternalReferencedOtherAssets.sql new file mode 100644 index 0000000..37bdf19 --- /dev/null +++ b/Analyzer/Resources/AddrBuildExplicitAssetInternalReferencedOtherAssets.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS addressables_build_explicit_asset_internal_referenced_other_assets +( + explicit_asset_id INTEGER, + build_id INTEGER, + internal_referenced_other_asset_rid INTEGER, + PRIMARY KEY (explicit_asset_id, build_id, internal_referenced_other_asset_rid), + FOREIGN KEY (explicit_asset_id, build_id) REFERENCES addressables_build_explicit_assets(id, build_id) +); \ No newline at end of file diff --git a/Analyzer/Resources/AddrBuildExplicitAssetLabels.sql b/Analyzer/Resources/AddrBuildExplicitAssetLabels.sql new file mode 100644 index 0000000..f01085d --- /dev/null +++ b/Analyzer/Resources/AddrBuildExplicitAssetLabels.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS addressables_build_explicit_asset_labels +( + explicit_asset_id INTEGER, + build_id INTEGER, + label TEXT, + PRIMARY KEY (explicit_asset_id, build_id, label), + FOREIGN KEY (explicit_asset_id, build_id) REFERENCES addressables_build_explicit_assets(id, build_id) +); \ No newline at end of file diff --git a/Analyzer/Resources/AddrBuildExplicitAssets.sql b/Analyzer/Resources/AddrBuildExplicitAssets.sql new file mode 100644 index 0000000..ba87bc9 --- /dev/null +++ b/Analyzer/Resources/AddrBuildExplicitAssets.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS addressables_build_explicit_assets +( + id INTEGER, + build_id INTEGER, + bundle INTEGER, + file INTEGER, + asset_hash TEXT, + asset_path TEXT, + addressable_name TEXT, + group_guid TEXT, + guid TEXT, + internal_id TEXT, + main_asset_type INTEGER, + serialized_size INTEGER, + streamed_size INTEGER, + PRIMARY KEY (id, build_id) +); \ No newline at end of file diff --git a/Analyzer/Resources/AddrBuildFileAssets.sql b/Analyzer/Resources/AddrBuildFileAssets.sql new file mode 100644 index 0000000..b7fd8a7 --- /dev/null +++ b/Analyzer/Resources/AddrBuildFileAssets.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS addressables_build_file_assets +( + file_id INTEGER, + build_id INTEGER, + asset_rid INTEGER, + PRIMARY KEY (file_id, build_id, asset_rid), + FOREIGN KEY (file_id, build_id) REFERENCES addressables_build_files(id, build_id) +); \ No newline at end of file diff --git a/Analyzer/Resources/AddrBuildFileExternalReferences.sql b/Analyzer/Resources/AddrBuildFileExternalReferences.sql new file mode 100644 index 0000000..127145e --- /dev/null +++ b/Analyzer/Resources/AddrBuildFileExternalReferences.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS addressables_build_file_external_references +( + file_id INTEGER, + build_id INTEGER, + external_reference_rid INTEGER, + PRIMARY KEY (file_id, build_id, external_reference_rid), + FOREIGN KEY (file_id, build_id) REFERENCES addressables_build_files(id, build_id) +); \ No newline at end of file diff --git a/Analyzer/Resources/AddrBuildFileOtherAssets.sql b/Analyzer/Resources/AddrBuildFileOtherAssets.sql new file mode 100644 index 0000000..124c93c --- /dev/null +++ b/Analyzer/Resources/AddrBuildFileOtherAssets.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS addressables_build_file_other_assets +( + file_id INTEGER, + build_id INTEGER, + other_asset_rid INTEGER, + PRIMARY KEY (file_id, build_id, other_asset_rid), + FOREIGN KEY (file_id, build_id) REFERENCES addressables_build_files(id, build_id) +); \ No newline at end of file diff --git a/Analyzer/Resources/AddrBuildFileSubFiles.sql b/Analyzer/Resources/AddrBuildFileSubFiles.sql new file mode 100644 index 0000000..707351f --- /dev/null +++ b/Analyzer/Resources/AddrBuildFileSubFiles.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS addressables_build_file_sub_files +( + file_id INTEGER, + build_id INTEGER, + sub_file_rid INTEGER, + PRIMARY KEY (file_id, build_id, sub_file_rid), + FOREIGN KEY (file_id, build_id) REFERENCES addressables_build_files(id, build_id) +); \ No newline at end of file diff --git a/Analyzer/Resources/AddrBuildFiles.sql b/Analyzer/Resources/AddrBuildFiles.sql new file mode 100644 index 0000000..3925e03 --- /dev/null +++ b/Analyzer/Resources/AddrBuildFiles.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS addressables_build_files +( + id INTEGER, + build_id INTEGER, + bundle INTEGER, + bundle_object_info_size INTEGER, + mono_script_count INTEGER, + mono_script_size INTEGER, + name TEXT, + preload_info_size INTEGER, + write_result_filename TEXT, + PRIMARY KEY (id, build_id) +); \ No newline at end of file diff --git a/Analyzer/Resources/AddrBuildGroupBundles.sql b/Analyzer/Resources/AddrBuildGroupBundles.sql new file mode 100644 index 0000000..30024ef --- /dev/null +++ b/Analyzer/Resources/AddrBuildGroupBundles.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS addressables_build_group_bundles +( + group_id INTEGER, + build_id INTEGER, + bundle_rid INTEGER, + PRIMARY KEY (group_id, build_id, bundle_rid), + FOREIGN KEY (group_id, build_id) REFERENCES addressables_build_groups(id, build_id) +); \ No newline at end of file diff --git a/Analyzer/Resources/AddrBuildGroupSchemas.sql b/Analyzer/Resources/AddrBuildGroupSchemas.sql new file mode 100644 index 0000000..87f9631 --- /dev/null +++ b/Analyzer/Resources/AddrBuildGroupSchemas.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS addressables_build_group_schemas +( + group_id INTEGER, + build_id INTEGER, + schema_rid INTEGER, + PRIMARY KEY (group_id, build_id, schema_rid), + FOREIGN KEY (group_id, build_id) REFERENCES addressables_build_groups(id, build_id) +); \ No newline at end of file diff --git a/Analyzer/Resources/AddrBuildGroups.sql b/Analyzer/Resources/AddrBuildGroups.sql new file mode 100644 index 0000000..61ad94f --- /dev/null +++ b/Analyzer/Resources/AddrBuildGroups.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS addressables_build_groups +( + id INTEGER, + build_id INTEGER, + guid TEXT, + name TEXT, + packing_mode TEXT, + PRIMARY KEY (id, build_id) +); \ No newline at end of file diff --git a/Analyzer/Resources/AddrBuildSchemaDataPairs.sql b/Analyzer/Resources/AddrBuildSchemaDataPairs.sql new file mode 100644 index 0000000..7e4d43b --- /dev/null +++ b/Analyzer/Resources/AddrBuildSchemaDataPairs.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS addressables_build_schema_data_pairs +( + schema_id INTEGER, + build_id INTEGER, + key TEXT, + value TEXT, + PRIMARY KEY (schema_id, build_id, key), + FOREIGN KEY (schema_id, build_id) REFERENCES addressables_build_schemas(id, build_id) +); \ No newline at end of file diff --git a/Analyzer/Resources/AddrBuildSchemas.sql b/Analyzer/Resources/AddrBuildSchemas.sql new file mode 100644 index 0000000..7dba756 --- /dev/null +++ b/Analyzer/Resources/AddrBuildSchemas.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS addressables_build_schemas +( + id INTEGER, + build_id INTEGER, + guid TEXT, + type TEXT, + PRIMARY KEY (id, build_id) +); \ No newline at end of file diff --git a/Analyzer/Resources/AddrBuildSubFiles.sql b/Analyzer/Resources/AddrBuildSubFiles.sql new file mode 100644 index 0000000..f11b1fb --- /dev/null +++ b/Analyzer/Resources/AddrBuildSubFiles.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS addressables_build_sub_files +( + id INTEGER, + build_id INTEGER, + is_serialized_file INTEGER, + name TEXT, + size INTEGER, + PRIMARY KEY (id, build_id) +); \ No newline at end of file diff --git a/Analyzer/Resources/AddrBuilds.sql b/Analyzer/Resources/AddrBuilds.sql new file mode 100644 index 0000000..7e047f2 --- /dev/null +++ b/Analyzer/Resources/AddrBuilds.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS addressables_builds +( + id INTEGER, + name TEXT, + build_target INTEGER, + start_time TEXT, + duration REAL, + error TEXT, + package_version TEXT, + player_version TEXT, + build_script TEXT, + result_hash TEXT, + type INTEGER, + unity_version TEXT, + PRIMARY KEY (id) +); diff --git a/Analyzer/Resources/AnimationClip.sql b/Analyzer/Resources/AnimationClip.sql index 375cb66..3637968 100644 --- a/Analyzer/Resources/AnimationClip.sql +++ b/Analyzer/Resources/AnimationClip.sql @@ -1,4 +1,4 @@ -CREATE TABLE animation_clips( +CREATE TABLE IF NOT EXISTS animation_clips( id INTEGER, legacy INTEGER, events INTEGER, diff --git a/Analyzer/Resources/AssetBundle.sql b/Analyzer/Resources/AssetBundle.sql index 24b2f55..4e7afdb 100644 --- a/Analyzer/Resources/AssetBundle.sql +++ b/Analyzer/Resources/AssetBundle.sql @@ -1,20 +1,20 @@ -CREATE TABLE assets( +CREATE TABLE IF NOT EXISTS assets( object INTEGER, name TEXT ); -CREATE TABLE asset_dependencies( +CREATE TABLE IF NOT EXISTS asset_dependencies( object INTEGER, dependency INTEGER ); -CREATE VIEW asset_view AS +CREATE VIEW IF NOT EXISTS asset_view AS SELECT a.name AS asset_name, o.* FROM assets a INNER JOIN object_view o ON o.id = a.object; -CREATE VIEW asset_dependencies_view AS +CREATE VIEW IF NOT EXISTS asset_dependencies_view AS SELECT a.id, a.asset_name, a.asset_bundle, a.type, od.id dep_id, od.asset_bundle dep_asset_bundle, od.name dep_name, od.type dep_type FROM asset_view a INNER JOIN asset_dependencies d ON a.id = d.object diff --git a/Analyzer/Resources/AudioClip.sql b/Analyzer/Resources/AudioClip.sql index bc80b4b..998085a 100644 --- a/Analyzer/Resources/AudioClip.sql +++ b/Analyzer/Resources/AudioClip.sql @@ -1,16 +1,16 @@ -CREATE TABLE audio_load_types( +CREATE TABLE IF NOT EXISTS audio_load_types( id INTEGER, name TEXT, PRIMARY KEY (id) ); -CREATE TABLE audio_formats( +CREATE TABLE IF NOT EXISTS audio_formats( id INTEGER, name TEXT, PRIMARY KEY (id) ); -CREATE TABLE audio_clips( +CREATE TABLE IF NOT EXISTS audio_clips( id INTEGER, bits_per_sample INTEGER, frequency INTEGER, 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/Init.sql b/Analyzer/Resources/Init.sql index a41a44a..6f384b5 100644 --- a/Analyzer/Resources/Init.sql +++ b/Analyzer/Resources/Init.sql @@ -1,11 +1,11 @@ -CREATE TABLE types +CREATE TABLE IF NOT EXISTS types ( id INTEGER, name TEXT, PRIMARY KEY (id) ); -CREATE TABLE asset_bundles +CREATE TABLE IF NOT EXISTS asset_bundles ( id INTEGER, name TEXT, @@ -13,7 +13,7 @@ CREATE TABLE asset_bundles PRIMARY KEY (id) ); -CREATE TABLE serialized_files +CREATE TABLE IF NOT EXISTS serialized_files ( id INTEGER, asset_bundle INTEGER, @@ -21,7 +21,7 @@ CREATE TABLE serialized_files PRIMARY KEY (id) ); -CREATE TABLE objects +CREATE TABLE IF NOT EXISTS objects ( id INTEGER, object_id INTEGER, @@ -34,7 +34,7 @@ CREATE TABLE objects PRIMARY KEY (id) ); -CREATE TABLE refs +CREATE TABLE IF NOT EXISTS refs ( object INTEGER, referenced_object INTEGER, diff --git a/Analyzer/Resources/Mesh.sql b/Analyzer/Resources/Mesh.sql index c1d5ea2..16a1566 100644 --- a/Analyzer/Resources/Mesh.sql +++ b/Analyzer/Resources/Mesh.sql @@ -1,4 +1,4 @@ -CREATE TABLE meshes( +CREATE TABLE IF NOT EXISTS meshes( id INTEGER, sub_meshes INTEGER, blend_shapes INTEGER, 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/Resources/Shader.sql b/Analyzer/Resources/Shader.sql index 2b05f96..d633744 100644 --- a/Analyzer/Resources/Shader.sql +++ b/Analyzer/Resources/Shader.sql @@ -1,28 +1,28 @@ -CREATE TABLE shaders( +CREATE TABLE IF NOT EXISTS shaders( id INTEGER, decompressed_size INTEGER, unique_programs INTEGER, PRIMARY KEY (id) ); -CREATE TABLE shader_keywords( +CREATE TABLE IF NOT EXISTS shader_keywords( id INTEGER, keyword TEXT, PRIMARY KEY (id) ); -CREATE TABLE shader_subprogram_keywords( +CREATE TABLE IF NOT EXISTS shader_subprogram_keywords( subprogram_id INTEGER, keyword_id INTEGER ); -CREATE TABLE shader_apis( +CREATE TABLE IF NOT EXISTS shader_apis( id INTEGER, name TEXT, PRIMARY KEY (id) ); -CREATE TABLE shader_subprograms( +CREATE TABLE IF NOT EXISTS shader_subprograms( id INTEGER, shader INTEGER, sub_shader INTEGER, diff --git a/Analyzer/Resources/Texture2D.sql b/Analyzer/Resources/Texture2D.sql index 5a9ba02..80ea8ea 100644 --- a/Analyzer/Resources/Texture2D.sql +++ b/Analyzer/Resources/Texture2D.sql @@ -1,11 +1,11 @@ -CREATE TABLE texture_formats +CREATE TABLE IF NOT EXISTS texture_formats ( id INTEGER, name TEXT, PRIMARY KEY (id) ); -CREATE TABLE textures +CREATE TABLE IF NOT EXISTS textures ( id INTEGER, width INTEGER, diff --git a/Analyzer/SQLite/Commands/AbstractCommand.cs b/Analyzer/SQLite/Commands/AbstractCommand.cs new file mode 100644 index 0000000..48011e5 --- /dev/null +++ b/Analyzer/SQLite/Commands/AbstractCommand.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands +{ + internal abstract class AbstractCommand + { + protected abstract string TableName { get; } + protected abstract Dictionary Fields { get; } + + private SqliteCommand m_Command = new SqliteCommand(); + + protected virtual string DDLSource { get => null; } + + // run data definition language commands to create + // tables and views, run once at the beginning of creating + // the database + public void RunDDL(SqliteConnection database) + { + if (DDLSource == null) + return; + using var command = database.CreateCommand(); + command.CommandText = DDLSource; + command.ExecuteNonQuery(); + } + + public void CreateCommand(SqliteConnection database) + { + RunDDL(database); + + m_Command = database.CreateCommand(); + var commandText = new StringBuilder($"INSERT INTO {TableName} ("); + commandText.Append(string.Join(", ", Fields.Keys)); + commandText.Append(") VALUES (@"); + commandText.Append(string.Join(", @", Fields.Keys)); + commandText.Append(")"); + m_Command.CommandText = commandText.ToString(); + + foreach (var entry in Fields) + { + m_Command.Parameters.Add("@" + entry.Key, entry.Value); + } + } + public void SetValue(string key, object value) + { + string prefixedKey = $"@{key}"; + if (m_Command.Parameters.Contains(prefixedKey)) + { + m_Command.Parameters[prefixedKey].Value = value ?? DBNull.Value; + } + else + { + throw new ArgumentException($"Parameter '{key}' does not exist in the command."); + } + } + + public void SetTransaction(SqliteTransaction transaction) + { + m_Command.Transaction = transaction; + } + + public int ExecuteNonQuery() + { + return m_Command.ExecuteNonQuery(); + } + + public void Dispose() + { + m_Command?.Dispose(); + } + } +} diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuild.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuild.cs new file mode 100644 index 0000000..894481c --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuild.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + internal class AddressablesBuild : AbstractCommand + { + protected override string TableName { get => "addressables_builds"; } + + protected override string DDLSource => Properties.Resources.AddrBuilds; + protected override Dictionary Fields + { + get => new Dictionary + { + { "name", SqliteType.Text }, + { "build_target", SqliteType.Integer }, + { "start_time", SqliteType.Text }, + { "duration", SqliteType.Real }, + { "error", SqliteType.Text }, + { "package_version", SqliteType.Text }, + { "player_version", SqliteType.Text }, + { "build_script", SqliteType.Text }, + { "result_hash", SqliteType.Text }, + { "type", SqliteType.Integer }, + { "unity_version", SqliteType.Text } + }; + } + public AddressablesBuild() + { + + } + + + } +} diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildBundle.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildBundle.cs new file mode 100644 index 0000000..3d51ed5 --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildBundle.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + /* TABLE DEFINITION: + create table addressables_build_bundles + ( + id INTEGER, + build_id INTEGER, + asset_count INTEGER, + build_status INTEGER, + crc INTEGER, + compression TEXT, + dependency_file_size INTEGER, + expanded_dependency_file_size INTEGER, + file_size INTEGER, + group_rid INTEGER, + hash TEXT, + internal_name TEXT, + load_path TEXT, + name TEXT, + provider TEXT, + result_type TEXT, + PRIMARY KEY (id, build_id) + ); + */ + internal class AddressablesBuildBundle : AbstractCommand + { + protected override string TableName => "addressables_build_bundles"; + + protected override string DDLSource => Properties.Resources.AddrBuildBundles; + + protected override Dictionary Fields => new Dictionary + { + { "id", SqliteType.Integer }, + { "build_id", SqliteType.Integer }, + { "asset_count", SqliteType.Integer }, + { "build_status", SqliteType.Integer }, + { "crc", SqliteType.Integer }, + { "compression", SqliteType.Text }, + { "dependency_file_size", SqliteType.Integer }, + { "expanded_dependency_file_size", SqliteType.Integer }, + { "file_size", SqliteType.Integer }, + { "group_rid", SqliteType.Integer }, + { "hash", SqliteType.Text }, // JSON object stored as TEXT + { "internal_name", SqliteType.Text }, + { "load_path", SqliteType.Text }, + { "name", SqliteType.Text }, + { "provider", SqliteType.Text }, + { "result_type", SqliteType.Text } + }; + + public AddressablesBuildBundle() + { + } + } +} diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildBundleDependency.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildBundleDependency.cs new file mode 100644 index 0000000..5bdab18 --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildBundleDependency.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + /* TABLE DEFINITION: + create table addressables_build_bundle_dependencies + ( + bundle_id INTEGER, + build_id INTEGER, + dependency_rid INTEGER, + PRIMARY KEY (bundle_id, build_id, dependency_rid), + FOREIGN KEY (bundle_id, build_id) REFERENCES addressables_build_bundles(id, build_id) + ); + */ + internal class AddressablesBuildBundleDependency : AbstractCommand + { + protected override string TableName => "addressables_build_bundle_dependencies"; + + protected override string DDLSource => Properties.Resources.AddrBuildBundleDependencies; + + protected override Dictionary Fields => new Dictionary + { + { "bundle_id", SqliteType.Integer }, + { "build_id", SqliteType.Integer }, + { "dependency_rid", SqliteType.Integer } + }; + + public AddressablesBuildBundleDependency() + { + } + } +} diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildBundleDependentBundle.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildBundleDependentBundle.cs new file mode 100644 index 0000000..24079bf --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildBundleDependentBundle.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + /* TABLE DEFINITION: + create table addressables_build_bundle_dependent_bundles + ( + bundle_id INTEGER, + build_id INTEGER, + dependent_bundle_rid INTEGER, + PRIMARY KEY (bundle_id, build_id, dependent_bundle_rid), + FOREIGN KEY (bundle_id, build_id) REFERENCES addressables_build_bundles(id, build_id) + ); + */ + internal class AddressablesBuildBundleDependentBundle : AbstractCommand + { + protected override string TableName => "addressables_build_bundle_dependent_bundles"; + + protected override string DDLSource => Properties.Resources.AddrBuildBundleDependentBundles; + + protected override Dictionary Fields => new Dictionary + { + { "bundle_id", SqliteType.Integer }, + { "build_id", SqliteType.Integer }, + { "dependent_bundle_rid", SqliteType.Integer } + }; + + public AddressablesBuildBundleDependentBundle() + { + } + } +} + diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildBundleExpandedDependency.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildBundleExpandedDependency.cs new file mode 100644 index 0000000..c7c11de --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildBundleExpandedDependency.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + /* TABLE DEFINITION: + create table addressables_build_bundle_expanded_dependencies + ( + bundle_id INTEGER, + build_id INTEGER, + dependency_rid INTEGER, + PRIMARY KEY (bundle_id, build_id, dependency_rid), + FOREIGN KEY (bundle_id, build_id) REFERENCES addressables_build_bundles(id, build_id) + ); + */ + internal class AddressablesBuildBundleExpandedDependency : AbstractCommand + { + protected override string TableName => "addressables_build_bundle_expanded_dependencies"; + + protected override string DDLSource => Properties.Resources.AddrBuildBundleExpandedDependencies; + protected override Dictionary Fields => new Dictionary + { + { "bundle_id", SqliteType.Integer }, + { "build_id", SqliteType.Integer }, + { "dependency_rid", SqliteType.Integer } + }; + + public AddressablesBuildBundleExpandedDependency() + { + } + } +} diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildBundleFile.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildBundleFile.cs new file mode 100644 index 0000000..8fbb02d --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildBundleFile.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + /* TABLE DEFINITION: + create table addressables_build_bundle_files + ( + bundle_id INTEGER, + build_id INTEGER, + file_rid INTEGER, + PRIMARY KEY (bundle_id, build_id, file_rid), + FOREIGN KEY (bundle_id, build_id) REFERENCES addressables_build_bundles(id, build_id) + ); + */ + internal class AddressablesBuildBundleFile : AbstractCommand + { + protected override string TableName => "addressables_build_bundle_files"; + + protected override string DDLSource => Properties.Resources.AddrBuildBundleFiles; + + protected override Dictionary Fields => new Dictionary + { + { "bundle_id", SqliteType.Integer }, + { "build_id", SqliteType.Integer }, + { "file_rid", SqliteType.Integer } + }; + + public AddressablesBuildBundleFile() + { + } + } +} + diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildBundleRegularDependency.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildBundleRegularDependency.cs new file mode 100644 index 0000000..4249c27 --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildBundleRegularDependency.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + /* TABLE DEFINITION: + create table addressables_build_bundle_regular_dependencies + ( + bundle_id INTEGER, + build_id INTEGER, + dependency_rid INTEGER, + PRIMARY KEY (bundle_id, build_id, dependency_rid), + FOREIGN KEY (bundle_id, build_id) REFERENCES addressables_build_bundles(id, build_id) + ); + */ + internal class AddressablesBuildBundleRegularDependency : AbstractCommand + { + protected override string TableName => "addressables_build_bundle_regular_dependencies"; + + protected override string DDLSource => Properties.Resources.AddrBuildBundleRegularDependencies; + + protected override Dictionary Fields => new Dictionary + { + { "bundle_id", SqliteType.Integer }, + { "build_id", SqliteType.Integer }, + { "dependency_rid", SqliteType.Integer } + }; + + public AddressablesBuildBundleRegularDependency() + { + } + } +} + diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildDataFromOtherAsset.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildDataFromOtherAsset.cs new file mode 100644 index 0000000..13ed994 --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildDataFromOtherAsset.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + /* TABLE DEFINITION: + create table addressables_build_data_from_other_assets + ( + id INTEGER, + build_id INTEGER, + asset_guid TEXT, + asset_path TEXT, + file INTEGER, + main_asset_type INTEGER, + object_count INTEGER, + serialized_size INTEGER, + streamed_size INTEGER, + PRIMARY KEY (id, build_id) + ); + */ + internal class AddressablesBuildDataFromOtherAsset : AbstractCommand + { + protected override string TableName => "addressables_build_data_from_other_assets"; + + protected override string DDLSource => Properties.Resources.AddrBuildDataFromOtherAssets; + + protected override Dictionary Fields => new Dictionary + { + { "id", SqliteType.Integer }, + { "build_id", SqliteType.Integer }, + { "asset_guid", SqliteType.Text }, + { "asset_path", SqliteType.Text }, + { "file", SqliteType.Integer }, + { "main_asset_type", SqliteType.Integer }, + { "object_count", SqliteType.Integer }, + { "serialized_size", SqliteType.Integer }, + { "streamed_size", SqliteType.Integer } + }; + + public AddressablesBuildDataFromOtherAsset() + { + } + } +} diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildDataFromOtherAssetObject.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildDataFromOtherAssetObject.cs new file mode 100644 index 0000000..9dbe136 --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildDataFromOtherAssetObject.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + /* TABLE DEFINITION: + create table addressables_build_data_from_other_asset_objects + ( + data_from_other_asset_id INTEGER, + build_id INTEGER, + asset_type INTEGER, + component_name TEXT, + local_identifier_in_file INTEGER, + object_name TEXT, + serialized_size INTEGER, + streamed_size INTEGER, + PRIMARY KEY (data_from_other_asset_id, build_id, local_identifier_in_file), + FOREIGN KEY (data_from_other_asset_id, build_id) REFERENCES addressables_build_data_from_other_assets(id, build_id) + ); + */ + internal class AddressablesBuildDataFromOtherAssetObject : AbstractCommand + { + protected override string TableName => "addressables_build_data_from_other_asset_objects"; + + protected override string DDLSource => Properties.Resources.AddrBuildDataFromOtherAssetObjects; + + protected override Dictionary Fields => new Dictionary + { + { "data_from_other_asset_id", SqliteType.Integer }, + { "build_id", SqliteType.Integer }, + { "asset_type", SqliteType.Integer }, + { "component_name", SqliteType.Text }, + { "local_identifier_in_file", SqliteType.Integer }, + { "object_name", SqliteType.Text }, + { "serialized_size", SqliteType.Integer }, + { "streamed_size", SqliteType.Integer } + }; + + public AddressablesBuildDataFromOtherAssetObject() + { + } + } +} + diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildDataFromOtherAssetObjectReference.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildDataFromOtherAssetObjectReference.cs new file mode 100644 index 0000000..2d497d3 --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildDataFromOtherAssetObjectReference.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + /* TABLE DEFINITION: + create table addressables_build_data_from_other_asset_object_references + ( + data_from_other_asset_id INTEGER, + build_id INTEGER, + local_identifier_in_file INTEGER, + asset_id INTEGER, + object_id INTEGER, + PRIMARY KEY (data_from_other_asset_id, build_id, local_identifier_in_file, asset_id, object_id), + FOREIGN KEY (data_from_other_asset_id, build_id, local_identifier_in_file) REFERENCES addressables_build_data_from_other_asset_objects(data_from_other_asset_id, build_id, local_identifier_in_file) + ); + */ + internal class AddressablesBuildDataFromOtherAssetObjectReference : AbstractCommand + { + protected override string TableName => "addressables_build_data_from_other_asset_object_references"; + + protected override string DDLSource => Properties.Resources.AddrBuildDataFromOtherAssetObjectReferences; + protected override Dictionary Fields => new Dictionary + { + { "data_from_other_asset_id", SqliteType.Integer }, + { "build_id", SqliteType.Integer }, + { "local_identifier_in_file", SqliteType.Integer }, + { "asset_id", SqliteType.Integer }, + { "object_id", SqliteType.Integer } + }; + + public AddressablesBuildDataFromOtherAssetObjectReference() + { + } + } +} diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildDataFromOtherAssetReferencingAsset.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildDataFromOtherAssetReferencingAsset.cs new file mode 100644 index 0000000..fe9cb2b --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildDataFromOtherAssetReferencingAsset.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + /* TABLE DEFINITION: + create table addressables_build_data_from_other_asset_referencing_assets + ( + data_from_other_asset_id INTEGER, + build_id INTEGER, + referencing_asset_rid INTEGER, + PRIMARY KEY (data_from_other_asset_id, build_id, referencing_asset_rid), + FOREIGN KEY (data_from_other_asset_id, build_id) REFERENCES addressables_build_data_from_other_assets(id, build_id) + ); + */ + internal class AddressablesBuildDataFromOtherAssetReferencingAsset : AbstractCommand + { + protected override string TableName => "addressables_build_data_from_other_asset_referencing_assets"; + + protected override string DDLSource => Properties.Resources.AddrBuildDataFromOtherAssetReferencingAssets; + + protected override Dictionary Fields => new Dictionary + { + { "data_from_other_asset_id", SqliteType.Integer }, + { "build_id", SqliteType.Integer }, + { "referencing_asset_rid", SqliteType.Integer } + }; + + public AddressablesBuildDataFromOtherAssetReferencingAsset() + { + } + } +} + diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildExplicitAsset.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildExplicitAsset.cs new file mode 100644 index 0000000..37cc7f3 --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildExplicitAsset.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + /* TABLE DEFINITION: + create table addressables_build_explicit_assets + ( + id INTEGER, + build_id INTEGER, + bundle INTEGER, + file INTEGER, + asset_hash TEXT, + asset_path TEXT, + addressable_name TEXT, + group_guid TEXT, + guid TEXT, + internal_id TEXT, + main_asset_type INTEGER, + serialized_size INTEGER, + streamed_size INTEGER, + PRIMARY KEY (id, build_id) + ); + */ + internal class AddressablesBuildExplicitAsset : AbstractCommand + { + protected override string TableName => "addressables_build_explicit_assets"; + + protected override string DDLSource => Properties.Resources.AddrBuildExplicitAssets; + + protected override Dictionary Fields => new Dictionary + { + { "id", SqliteType.Integer }, + { "build_id", SqliteType.Integer}, + { "bundle", SqliteType.Integer}, + { "file", SqliteType.Integer }, + { "asset_hash", SqliteType.Text }, + { "asset_path", SqliteType.Text }, + { "addressable_name", SqliteType.Text }, + { "group_guid", SqliteType.Text }, + { "guid", SqliteType.Text }, + { "internal_id", SqliteType.Text }, + { "main_asset_type", SqliteType.Integer }, + { "streamed_size", SqliteType.Integer }, + { "serialized_size", SqliteType.Integer } + }; + public AddressablesBuildExplicitAsset() + { + } + } +} diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildExplicitAssetExternallyReferencedAsset.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildExplicitAssetExternallyReferencedAsset.cs new file mode 100644 index 0000000..672e404 --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildExplicitAssetExternallyReferencedAsset.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + /* TABLE DEFINITION: + create table addressables_build_explicit_asset_externally_referenced_assets + ( + explicit_asset_id INTEGER, + build_id INTEGER, + externally_referenced_asset_rid INTEGER, + PRIMARY KEY (explicit_asset_id, build_id, externally_referenced_asset_rid), + FOREIGN KEY (explicit_asset_id, build_id) REFERENCES addressables_build_explicit_assets(id, build_id) + ); + */ + internal class AddressablesBuildExplicitAssetExternallyReferencedAsset : AbstractCommand + { + protected override string TableName => "addressables_build_explicit_asset_externally_referenced_assets"; + + protected override string DDLSource => Properties.Resources.AddrBuildExplicitAssetExternallyReferencedAssets; + + protected override Dictionary Fields => new Dictionary + { + { "explicit_asset_id", SqliteType.Integer }, + { "build_id", SqliteType.Integer }, + { "externally_referenced_asset_rid", SqliteType.Integer } + }; + + public AddressablesBuildExplicitAssetExternallyReferencedAsset() + { + } + } +} + diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildExplicitAssetInternalReferencedExplicitAsset.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildExplicitAssetInternalReferencedExplicitAsset.cs new file mode 100644 index 0000000..cc28067 --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildExplicitAssetInternalReferencedExplicitAsset.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + /* TABLE DEFINITION: + create table addressables_build_explicit_asset_internal_referenced_explicit_assets + ( + explicit_asset_id INTEGER, + build_id INTEGER, + internal_referenced_explicit_asset_rid INTEGER, + PRIMARY KEY (explicit_asset_id, build_id, internal_referenced_explicit_asset_rid), + FOREIGN KEY (explicit_asset_id, build_id) REFERENCES addressables_build_explicit_assets(id, build_id) + ); + */ + internal class AddressablesBuildExplicitAssetInternalReferencedExplicitAsset : AbstractCommand + { + protected override string TableName => "addressables_build_explicit_asset_internal_referenced_explicit_assets"; + + protected override string DDLSource => Properties.Resources.AddrBuildExplicitAssetInternalReferencedExplicitAssets; + + protected override Dictionary Fields => new Dictionary + { + { "explicit_asset_id", SqliteType.Integer }, + { "build_id", SqliteType.Integer }, + { "internal_referenced_explicit_asset_rid", SqliteType.Integer } + }; + + public AddressablesBuildExplicitAssetInternalReferencedExplicitAsset() + { + } + } +} + diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildExplicitAssetInternalReferencedOtherAsset.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildExplicitAssetInternalReferencedOtherAsset.cs new file mode 100644 index 0000000..0152d42 --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildExplicitAssetInternalReferencedOtherAsset.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + /* TABLE DEFINITION: + create table addressables_build_explicit_asset_internal_referenced_other_assets + ( + explicit_asset_id INTEGER, + build_id INTEGER, + internal_referenced_other_asset_rid INTEGER, + PRIMARY KEY (explicit_asset_id, build_id, internal_referenced_other_asset_rid), + FOREIGN KEY (explicit_asset_id, build_id) REFERENCES addressables_build_explicit_assets(id, build_id) + ); + */ + internal class AddressablesBuildExplicitAssetInternalReferencedOtherAsset : AbstractCommand + { + protected override string TableName => "addressables_build_explicit_asset_internal_referenced_other_assets"; + + protected override string DDLSource => Properties.Resources.AddrBuildExplicitAssetInternalReferencedOtherAssets; + + protected override Dictionary Fields => new Dictionary + { + { "explicit_asset_id", SqliteType.Integer }, + { "build_id", SqliteType.Integer }, + { "internal_referenced_other_asset_rid", SqliteType.Integer } + }; + + public AddressablesBuildExplicitAssetInternalReferencedOtherAsset() + { + } + } +} + diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildExplicitAssetLabel.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildExplicitAssetLabel.cs new file mode 100644 index 0000000..195ef4e --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildExplicitAssetLabel.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + /* TABLE DEFINITION: + create table addressables_build_explicit_asset_labels + ( + explicit_asset_id INTEGER, + build_id INTEGER, + label TEXT, + PRIMARY KEY (explicit_asset_id, build_id, label), + FOREIGN KEY (explicit_asset_id, build_id) REFERENCES addressables_build_explicit_assets(id, build_id) + ); + */ + internal class AddressablesBuildExplicitAssetLabel : AbstractCommand + { + protected override string TableName => "addressables_build_explicit_asset_labels"; + + protected override string DDLSource => Properties.Resources.AddrBuildExplicitAssetLabels; + + protected override Dictionary Fields => new Dictionary + { + { "explicit_asset_id", SqliteType.Integer }, + { "build_id", SqliteType.Integer }, + { "label", SqliteType.Text } + }; + + public AddressablesBuildExplicitAssetLabel() + { + } + } +} + diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildFile.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildFile.cs new file mode 100644 index 0000000..49efbf4 --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildFile.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + /* TABLE DEFINITION: + create table addressables_build_files + ( + id INTEGER, + build_id INTEGER, + bundle INTEGER, + bundle_object_info_size INTEGER, + mono_script_count INTEGER, + mono_script_size INTEGER, + name TEXT, + preload_info_size INTEGER, + write_result_filename TEXT, + PRIMARY KEY (id, build_id) + ); + */ + internal class AddressablesBuildFile : AbstractCommand + { + protected override string TableName => "addressables_build_files"; + + protected override string DDLSource => Properties.Resources.AddrBuildFiles; + + protected override Dictionary Fields => new Dictionary + { + { "id", SqliteType.Integer }, + { "build_id", SqliteType.Integer }, + { "bundle", SqliteType.Integer }, + { "bundle_object_info_size", SqliteType.Integer }, + { "mono_script_count", SqliteType.Integer }, + { "mono_script_size", SqliteType.Integer }, + { "name", SqliteType.Text }, + { "preload_info_size", SqliteType.Integer }, + { "write_result_filename", SqliteType.Text } + }; + + public AddressablesBuildFile() + { + } + } +} diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildFileAsset.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildFileAsset.cs new file mode 100644 index 0000000..1acb192 --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildFileAsset.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + /* TABLE DEFINITION: + create table addressables_build_file_assets + ( + file_id INTEGER, + build_id INTEGER, + asset_rid INTEGER, + PRIMARY KEY (file_id, build_id, asset_rid), + FOREIGN KEY (file_id, build_id) REFERENCES addressables_build_files(id, build_id) + ); + */ + internal class AddressablesBuildFileAsset : AbstractCommand + { + protected override string TableName => "addressables_build_file_assets"; + + protected override string DDLSource => Properties.Resources.AddrBuildFileAssets; + + protected override Dictionary Fields => new Dictionary + { + { "file_id", SqliteType.Integer }, + { "build_id", SqliteType.Integer }, + { "asset_rid", SqliteType.Integer } + }; + + public AddressablesBuildFileAsset() + { + } + } +} + + diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildFileExternalReference.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildFileExternalReference.cs new file mode 100644 index 0000000..0aaa10a --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildFileExternalReference.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + /* TABLE DEFINITION: + create table addressables_build_file_external_references + ( + file_id INTEGER, + build_id INTEGER, + external_reference_rid INTEGER, + PRIMARY KEY (file_id, build_id, external_reference_rid), + FOREIGN KEY (file_id, build_id) REFERENCES addressables_build_files(id, build_id) + ); + */ + internal class AddressablesBuildFileExternalReference : AbstractCommand + { + protected override string TableName => "addressables_build_file_external_references"; + + protected override string DDLSource => Properties.Resources.AddrBuildFileExternalReferences; + + protected override Dictionary Fields => new Dictionary + { + { "file_id", SqliteType.Integer }, + { "build_id", SqliteType.Integer }, + { "external_reference_rid", SqliteType.Integer } + }; + + public AddressablesBuildFileExternalReference() + { + } + } +} + + diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildFileOtherAsset.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildFileOtherAsset.cs new file mode 100644 index 0000000..8db0c60 --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildFileOtherAsset.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + /* TABLE DEFINITION: + create table addressables_build_file_other_assets + ( + file_id INTEGER, + build_id INTEGER, + other_asset_rid INTEGER, + PRIMARY KEY (file_id, build_id, other_asset_rid), + FOREIGN KEY (file_id, build_id) REFERENCES addressables_build_files(id, build_id) + ); + */ + internal class AddressablesBuildFileOtherAsset : AbstractCommand + { + protected override string TableName => "addressables_build_file_other_assets"; + + protected override string DDLSource => Properties.Resources.AddrBuildFileOtherAssets; + + protected override Dictionary Fields => new Dictionary + { + { "file_id", SqliteType.Integer }, + { "build_id", SqliteType.Integer }, + { "other_asset_rid", SqliteType.Integer } + }; + + public AddressablesBuildFileOtherAsset() + { + } + } +} + + diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildFileSubFile.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildFileSubFile.cs new file mode 100644 index 0000000..fe16742 --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildFileSubFile.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + /* TABLE DEFINITION: + create table addressables_build_file_sub_files + ( + file_id INTEGER, + build_id INTEGER, + sub_file_rid INTEGER, + PRIMARY KEY (file_id, build_id, sub_file_rid), + FOREIGN KEY (file_id, build_id) REFERENCES addressables_build_files(id, build_id) + ); + */ + internal class AddressablesBuildFileSubFile : AbstractCommand + { + protected override string TableName => "addressables_build_file_sub_files"; + + protected override string DDLSource => Properties.Resources.AddrBuildFileSubFiles; + + protected override Dictionary Fields => new Dictionary + { + { "file_id", SqliteType.Integer }, + { "build_id", SqliteType.Integer }, + { "sub_file_rid", SqliteType.Integer } + }; + + public AddressablesBuildFileSubFile() + { + } + } +} + + diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildGroup.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildGroup.cs new file mode 100644 index 0000000..541f1d3 --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildGroup.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + /* TABLE DEFINITION: + create table addressables_build_groups + ( + id INTEGER, + build_id INTEGER, + guid TEXT, + name TEXT, + packing_mode TEXT, + PRIMARY KEY (id, build_id) + ); + */ + internal class AddressablesBuildGroup : AbstractCommand + { + protected override string TableName => "addressables_build_groups"; + + protected override string DDLSource => Properties.Resources.AddrBuildGroups; + + protected override Dictionary Fields => new Dictionary + { + { "id", SqliteType.Integer }, + { "build_id", SqliteType.Integer }, + { "guid", SqliteType.Text }, + { "name", SqliteType.Text }, + { "packing_mode", SqliteType.Text }, + }; + + public AddressablesBuildGroup() + { + } + } +} + diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildGroupBundle.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildGroupBundle.cs new file mode 100644 index 0000000..da39148 --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildGroupBundle.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + /* TABLE DEFINITION: + create table addressables_build_group_bundles + ( + group_id INTEGER, + build_id INTEGER, + bundle_rid INTEGER, + PRIMARY KEY (group_id, build_id, bundle_rid), + FOREIGN KEY (group_id, build_id) REFERENCES addressables_build_groups(id, build_id) + ); + */ + internal class AddressablesBuildGroupBundle : AbstractCommand + { + protected override string TableName => "addressables_build_group_bundles"; + + protected override string DDLSource => Properties.Resources.AddrBuildGroupBundles; + + protected override Dictionary Fields => new Dictionary + { + { "group_id", SqliteType.Integer }, + { "build_id", SqliteType.Integer }, + { "bundle_rid", SqliteType.Integer } + }; + + public AddressablesBuildGroupBundle() + { + } + } +} + diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildGroupSchema.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildGroupSchema.cs new file mode 100644 index 0000000..6bd9945 --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildGroupSchema.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + /* TABLE DEFINITION: + create table addressables_build_group_schemas + ( + group_id INTEGER, + build_id INTEGER, + schema_rid INTEGER, + PRIMARY KEY (group_id, build_id, schema_rid), + FOREIGN KEY (group_id, build_id) REFERENCES addressables_build_groups(id, build_id) + ); + */ + internal class AddressablesBuildGroupSchema : AbstractCommand + { + protected override string TableName => "addressables_build_group_schemas"; + + protected override string DDLSource => Properties.Resources.AddrBuildGroupSchemas; + + protected override Dictionary Fields => new Dictionary + { + { "group_id", SqliteType.Integer }, + { "build_id", SqliteType.Integer }, + { "schema_rid", SqliteType.Integer } + }; + + public AddressablesBuildGroupSchema() + { + } + } +} + diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildSchema.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildSchema.cs new file mode 100644 index 0000000..5381388 --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildSchema.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + /* TABLE DEFINITION: + create table addressables_build_schemas + ( + id INTEGER, + build_id INTEGER, + guid TEXT, + type TEXT, + PRIMARY KEY (id, build_id) + ); + */ + internal class AddressablesBuildSchema : AbstractCommand + { + protected override string TableName => "addressables_build_schemas"; + + protected override string DDLSource => Properties.Resources.AddrBuildSchemas; + + protected override Dictionary Fields => new Dictionary + { + { "id", SqliteType.Integer }, + { "build_id", SqliteType.Integer }, + { "guid", SqliteType.Text }, + { "type", SqliteType.Text } + }; + + public AddressablesBuildSchema() + { + } + } +} + diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildSchemaDataPair.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildSchemaDataPair.cs new file mode 100644 index 0000000..ff92607 --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildSchemaDataPair.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + /* TABLE DEFINITION: + create table addressables_build_schema_data_pairs + ( + schema_id INTEGER, + build_id INTEGER, + key TEXT, + value TEXT, + PRIMARY KEY (schema_id, build_id, key), + FOREIGN KEY (schema_id, build_id) REFERENCES addressables_build_schemas(id, build_id) + ); + */ + internal class AddressablesBuildSchemaDataPair : AbstractCommand + { + protected override string TableName => "addressables_build_schema_data_pairs"; + + protected override string DDLSource => Properties.Resources.AddrBuildSchemaDataPairs; + + protected override Dictionary Fields => new Dictionary + { + { "schema_id", SqliteType.Integer }, + { "build_id", SqliteType.Integer }, + { "key", SqliteType.Text }, + { "value", SqliteType.Text } + }; + + public AddressablesBuildSchemaDataPair() + { + } + } +} diff --git a/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildSubFile.cs b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildSubFile.cs new file mode 100644 index 0000000..b6ea452 --- /dev/null +++ b/Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesBuildSubFile.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport +{ + /* TABLE DEFINITION: + create table addressables_build_sub_files + ( + id INTEGER, + build_id INTEGER, + is_serialized_file INTEGER, + name TEXT, + size INTEGER, + PRIMARY KEY (id, build_id) + ); + */ + internal class AddressablesBuildSubFile : AbstractCommand + { + protected override string TableName => "addressables_build_sub_files"; + + protected override string DDLSource => Properties.Resources.AddrBuildSubFiles; + + protected override Dictionary Fields => new Dictionary + { + { "id", SqliteType.Integer }, + { "build_id", SqliteType.Integer }, + { "is_serialized_file", SqliteType.Integer }, + { "name", SqliteType.Text }, + { "size", SqliteType.Integer } + }; + + public AddressablesBuildSubFile() + { + } + } +} diff --git a/Analyzer/SQLite/Commands/SerializedFile/AddAssetBundle.cs b/Analyzer/SQLite/Commands/SerializedFile/AddAssetBundle.cs new file mode 100644 index 0000000..3938e0f --- /dev/null +++ b/Analyzer/SQLite/Commands/SerializedFile/AddAssetBundle.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Commands.SerializedFile +{ + /* TABLE DEFINITION: + create table asset_bundles + ( + id INTEGER, + name TEXT, + file_size INTEGER, + PRIMARY KEY (id) + ); + */ + internal class AddAssetBundle : AbstractCommand + { + protected override string TableName => "asset_bundles"; + + protected override string DDLSource => null; + + protected override Dictionary Fields => new() + { + { "id", SqliteType.Integer }, + { "name", SqliteType.Text }, + { "file_size", SqliteType.Integer } + }; + } +} diff --git a/Analyzer/SQLite/Commands/SerializedFile/AddAssetDependency.cs b/Analyzer/SQLite/Commands/SerializedFile/AddAssetDependency.cs new file mode 100644 index 0000000..3143abd --- /dev/null +++ b/Analyzer/SQLite/Commands/SerializedFile/AddAssetDependency.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; +using UnityDataTools.Analyzer.SQLite.Commands; + +namespace UnityDataTools.Analyzer.SQLite.Commands.SerializedFile +{ + /* TABLE DEFINITION: + create table asset_dependencies + ( + object INTEGER, + dependency INTEGER, + PRIMARY KEY (object, dependency) + ); + */ + internal class AddAssetDependency : AbstractCommand + { + protected override string TableName => "asset_dependencies"; + + protected override string DDLSource => Resources.AssetBundle; + + protected override Dictionary Fields => new() + { + { "object", SqliteType.Integer }, + { "dependency", SqliteType.Integer } + }; + } +} diff --git a/Analyzer/SQLite/Commands/SerializedFile/AddObject.cs b/Analyzer/SQLite/Commands/SerializedFile/AddObject.cs new file mode 100644 index 0000000..9ff1034 --- /dev/null +++ b/Analyzer/SQLite/Commands/SerializedFile/AddObject.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; +using UnityDataTools.Analyzer.SQLite.Commands; + +namespace UnityDataTools.Analyzer.SQLite.Commands.SerializedFile +{ + /* TABLE DEFINITION: + create table objects + ( + id INTEGER, + object_id INTEGER, + serialized_file INTEGER, + type INTEGER, + name TEXT, + game_object INTEGER, + size INTEGER, + crc32 INTEGER, + PRIMARY KEY (id) + ); + */ + internal class AddObject : AbstractCommand + { + protected override string TableName => "objects"; + + protected override string DDLSource => null; + protected override Dictionary Fields => new() + { + { "id", SqliteType.Integer }, + { "object_id", SqliteType.Integer }, + { "serialized_file", SqliteType.Integer }, + { "type", SqliteType.Integer }, + { "name", SqliteType.Text }, + { "game_object", SqliteType.Integer }, + { "size", SqliteType.Integer }, + { "crc32", SqliteType.Integer } + }; + } +} diff --git a/Analyzer/SQLite/Commands/SerializedFile/AddReference.cs b/Analyzer/SQLite/Commands/SerializedFile/AddReference.cs new file mode 100644 index 0000000..8352129 --- /dev/null +++ b/Analyzer/SQLite/Commands/SerializedFile/AddReference.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; +using UnityDataTools.Analyzer.SQLite.Commands; + +namespace UnityDataTools.Analyzer.SQLite.Commands.SerializedFile +{ + /* TABLE DEFINITION: + create table refs + ( + object INTEGER, + referenced_object INTEGER, + property_path TEXT, + property_type TEXT, + PRIMARY KEY (object, referenced_object, property_path) + ); + */ + internal class AddReference : AbstractCommand + { + protected override string TableName => "refs"; + + protected override string DDLSource => null; + + protected override Dictionary Fields => new() + { + { "object", SqliteType.Integer }, + { "referenced_object", SqliteType.Integer }, + { "property_path", SqliteType.Text }, + { "property_type", SqliteType.Text } + }; + } +} diff --git a/Analyzer/SQLite/Commands/SerializedFile/AddSerializedFile.cs b/Analyzer/SQLite/Commands/SerializedFile/AddSerializedFile.cs new file mode 100644 index 0000000..00778bc --- /dev/null +++ b/Analyzer/SQLite/Commands/SerializedFile/AddSerializedFile.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; +using UnityDataTools.Analyzer.SQLite.Commands; + +namespace UnityDataTools.Analyzer.SQLite.Commands.SerializedFile +{ + /* TABLE DEFINITION: + create table serialized_files + ( + id INTEGER, + asset_bundle INTEGER, + name TEXT, + PRIMARY KEY (id) + ); + */ + internal class AddSerializedFile : AbstractCommand + { + protected override string TableName => "serialized_files"; + + protected override string DDLSource => null; + + protected override Dictionary Fields => new() + { + { "id", SqliteType.Integer }, + { "asset_bundle", SqliteType.Integer }, + { "name", SqliteType.Text } + }; + } +} diff --git a/Analyzer/SQLite/Commands/SerializedFile/AddType.cs b/Analyzer/SQLite/Commands/SerializedFile/AddType.cs new file mode 100644 index 0000000..859b360 --- /dev/null +++ b/Analyzer/SQLite/Commands/SerializedFile/AddType.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; +using UnityDataTools.Analyzer.SQLite.Commands; + +namespace UnityDataTools.Analyzer.SQLite.Commands.SerializedFile +{ + /* TABLE DEFINITION: + create table types + ( + id INTEGER, + name TEXT, + PRIMARY KEY (id) + ); + */ + internal class AddType : AbstractCommand + { + protected override string TableName => "types"; + + protected override string DDLSource => null; + + protected override Dictionary Fields => new() + { + { "id", SqliteType.Integer }, + { "name", SqliteType.Text } + }; + } +} diff --git a/Analyzer/SQLite/Handlers/AnimationClipHandler.cs b/Analyzer/SQLite/Handlers/AnimationClipHandler.cs index 28d4a7d..04cf62f 100644 --- a/Analyzer/SQLite/Handlers/AnimationClipHandler.cs +++ b/Analyzer/SQLite/Handlers/AnimationClipHandler.cs @@ -1,34 +1,32 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Data.SQLite; +using System; +using Microsoft.Data.Sqlite; using UnityDataTools.Analyzer.SerializedObjects; using UnityDataTools.FileSystem.TypeTreeReaders; + namespace UnityDataTools.Analyzer.SQLite.Handlers; public class AnimationClipHandler : ISQLiteHandler { - SQLiteCommand m_InsertCommand; + SqliteCommand m_InsertCommand; - public void Init(SQLiteConnection db) + public void Init(SqliteConnection db) { - using var command = new SQLiteCommand(db); - - command.CommandText = Properties.Resources.AnimationClip; + using var command = db.CreateCommand(); + command.CommandText = Resources.AnimationClip; command.ExecuteNonQuery(); - m_InsertCommand = new SQLiteCommand(db); + m_InsertCommand = db.CreateCommand(); m_InsertCommand.CommandText = "INSERT INTO animation_clips(id, legacy, events) VALUES(@id, @legacy, @events)"; - m_InsertCommand.Parameters.Add("@id", DbType.Int64); - m_InsertCommand.Parameters.Add("@legacy", DbType.Int32); - m_InsertCommand.Parameters.Add("@events", DbType.Int32); + m_InsertCommand.Parameters.Add("@id", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@legacy", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@events", SqliteType.Integer); } public void Process(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize) { var animationClip = AnimationClip.Read(reader); - + m_InsertCommand.Transaction = ctx.Transaction; m_InsertCommand.Parameters["@id"].Value = objectId; m_InsertCommand.Parameters["@legacy"].Value = animationClip.Legacy; m_InsertCommand.Parameters["@events"].Value = animationClip.Events; @@ -38,12 +36,12 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s streamDataSize = 0; } - public void Finalize(SQLiteConnection db) + public void Finalize(SqliteConnection db) { } void IDisposable.Dispose() { - m_InsertCommand.Dispose(); + m_InsertCommand?.Dispose(); } -} \ No newline at end of file +} diff --git a/Analyzer/SQLite/Handlers/AssetBundleHandler.cs b/Analyzer/SQLite/Handlers/AssetBundleHandler.cs index 90818b0..d62695e 100644 --- a/Analyzer/SQLite/Handlers/AssetBundleHandler.cs +++ b/Analyzer/SQLite/Handlers/AssetBundleHandler.cs @@ -1,7 +1,6 @@ -using System; -using System.Data; -using System.Data.SQLite; +using System; using System.Text.RegularExpressions; +using Microsoft.Data.Sqlite; using UnityDataTools.Analyzer.SerializedObjects; using UnityDataTools.FileSystem.TypeTreeReaders; @@ -9,39 +8,40 @@ namespace UnityDataTools.Analyzer.SQLite.Handlers; public class AssetBundleHandler : ISQLiteHandler { - SQLiteCommand m_InsertCommand; - private SQLiteCommand m_InsertDepCommand; + SqliteCommand m_InsertCommand; + private SqliteCommand m_InsertDepCommand; private Regex m_SceneNameRegex = new Regex(@"([^//]+)\.unity"); - public void Init(SQLiteConnection db) + public void Init(SqliteConnection db) { - using var command = new SQLiteCommand(db); - - command.CommandText = Properties.Resources.AssetBundle; + using var command = db.CreateCommand(); + command.CommandText = Resources.AssetBundle; command.ExecuteNonQuery(); - m_InsertCommand = new SQLiteCommand(db); + m_InsertCommand = db.CreateCommand(); + m_InsertCommand.CommandText = "INSERT INTO assets(object, name) VALUES(@object, @name)"; - m_InsertCommand.Parameters.Add("@object", DbType.Int64); - m_InsertCommand.Parameters.Add("@name", DbType.String); - - m_InsertDepCommand = new SQLiteCommand(db); + m_InsertCommand.Parameters.Add("@object", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@name", SqliteType.Text); + + m_InsertDepCommand = db.CreateCommand(); + m_InsertDepCommand.CommandText = "INSERT INTO asset_dependencies(object, dependency) VALUES(@object, @dependency)"; - m_InsertDepCommand.Parameters.Add("@object", DbType.Int64); - m_InsertDepCommand.Parameters.Add("@dependency", DbType.Int64); + m_InsertDepCommand.Parameters.Add("@object", SqliteType.Integer); + m_InsertDepCommand.Parameters.Add("@dependency", SqliteType.Integer); } public void Process(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize) { var assetBundle = AssetBundle.Read(reader); - + foreach (var asset in assetBundle.Assets) { if (!assetBundle.IsSceneAssetBundle) { var fileId = ctx.LocalToDbFileId[asset.PPtr.FileId]; var objId = ctx.ObjectIdProvider.GetId((fileId, asset.PPtr.PathId)); - + m_InsertCommand.Transaction = ctx.Transaction; m_InsertCommand.Parameters["@object"].Value = objId; m_InsertCommand.Parameters["@name"].Value = asset.Name; m_InsertCommand.ExecuteNonQuery(); @@ -51,7 +51,7 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s var dependency = assetBundle.PreloadTable[i]; var depFileId = ctx.LocalToDbFileId[dependency.FileId]; var depId = ctx.ObjectIdProvider.GetId((depFileId, dependency.PathId)); - + m_InsertDepCommand.Transaction = ctx.Transaction; m_InsertDepCommand.Parameters["@object"].Value = objId; m_InsertDepCommand.Parameters["@dependency"].Value = depId; m_InsertDepCommand.ExecuteNonQuery(); @@ -65,7 +65,7 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s { var sceneName = match.Groups[1].Value; var objId = ctx.ObjectIdProvider.GetId((ctx.SerializedFileIdProvider.GetId(sceneName), 0)); - + m_InsertCommand.Transaction = ctx.Transaction; m_InsertCommand.Parameters["@object"].Value = objId; m_InsertCommand.Parameters["@name"].Value = asset.Name; m_InsertCommand.ExecuteNonQuery(); @@ -77,20 +77,20 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s streamDataSize = 0; } - public void Finalize(SQLiteConnection db) + public void Finalize(SqliteConnection db) { - using var command = new SQLiteCommand(db); - + using var command = new SqliteCommand(); + command.Connection = db; command.CommandText = "CREATE INDEX asset_dependencies_object ON asset_dependencies(object)"; command.ExecuteNonQuery(); - + command.CommandText = "CREATE INDEX asset_dependencies_dependency ON asset_dependencies(dependency)"; command.ExecuteNonQuery(); } void IDisposable.Dispose() { - m_InsertCommand.Dispose(); - m_InsertDepCommand.Dispose(); + m_InsertCommand?.Dispose(); + m_InsertDepCommand?.Dispose(); } -} \ No newline at end of file +} diff --git a/Analyzer/SQLite/Handlers/AudioClipHandler.cs b/Analyzer/SQLite/Handlers/AudioClipHandler.cs index 8428d5d..a4119ec 100644 --- a/Analyzer/SQLite/Handlers/AudioClipHandler.cs +++ b/Analyzer/SQLite/Handlers/AudioClipHandler.cs @@ -1,7 +1,6 @@ -using System; -using System.Collections.Generic; +using System; using System.Data; -using System.Data.SQLite; +using Microsoft.Data.Sqlite; using UnityDataTools.Analyzer.SerializedObjects; using UnityDataTools.FileSystem.TypeTreeReaders; @@ -9,29 +8,28 @@ namespace UnityDataTools.Analyzer.SQLite.Handlers; public class AudioClipHandler : ISQLiteHandler { - SQLiteCommand m_InsertCommand; + private SqliteCommand m_InsertCommand; - public void Init(SQLiteConnection db) + public void Init(SqliteConnection db) { - using var command = new SQLiteCommand(db); - - command.CommandText = Properties.Resources.AudioClip; + using var command = db.CreateCommand(); + command.CommandText = Resources.AudioClip; command.ExecuteNonQuery(); - m_InsertCommand = new SQLiteCommand(db); + m_InsertCommand = db.CreateCommand(); m_InsertCommand.CommandText = "INSERT INTO audio_clips(id, bits_per_sample, frequency, channels, load_type, format) VALUES(@id, @bits_per_sample, @frequency, @channels, @load_type, @format)"; - m_InsertCommand.Parameters.Add("@id", DbType.Int64); - m_InsertCommand.Parameters.Add("@bits_per_sample", DbType.Int32); - m_InsertCommand.Parameters.Add("@frequency", DbType.Int32); - m_InsertCommand.Parameters.Add("@channels", DbType.Int32); - m_InsertCommand.Parameters.Add("@load_type", DbType.Int32); - m_InsertCommand.Parameters.Add("@format", DbType.Int32); + m_InsertCommand.Parameters.Add("@id", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@bits_per_sample", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@frequency", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@channels", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@load_type", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@format", SqliteType.Integer); } public void Process(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize) { var audioClip = AudioClip.Read(reader); - + m_InsertCommand.Transaction = ctx.Transaction; m_InsertCommand.Parameters["@id"].Value = objectId; m_InsertCommand.Parameters["@bits_per_sample"].Value = audioClip.BitsPerSample; m_InsertCommand.Parameters["@frequency"].Value = audioClip.Frequency; @@ -45,12 +43,12 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s name = audioClip.Name; } - public void Finalize(SQLiteConnection db) + public void Finalize(SqliteConnection db) { } void IDisposable.Dispose() { - m_InsertCommand.Dispose(); + m_InsertCommand?.Dispose(); } -} \ No newline at end of file +} 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/ISQLiteHandler.cs b/Analyzer/SQLite/Handlers/ISQLiteHandler.cs index 660762b..147e15e 100644 --- a/Analyzer/SQLite/Handlers/ISQLiteHandler.cs +++ b/Analyzer/SQLite/Handlers/ISQLiteHandler.cs @@ -1,6 +1,6 @@ -using System; +using System; using System.Collections.Generic; -using System.Data.SQLite; +using Microsoft.Data.Sqlite; using UnityDataTools.FileSystem.TypeTreeReaders; namespace UnityDataTools.Analyzer.SQLite.Handlers; @@ -13,11 +13,20 @@ public class Context public Util.ObjectIdProvider ObjectIdProvider { get; init; } public Util.IdProvider SerializedFileIdProvider { get; init; } public Dictionary LocalToDbFileId { get; init; } + public SqliteTransaction Transaction { get; set; } } public interface ISQLiteHandler : IDisposable { - void Init(SQLiteConnection db); + void Init(Microsoft.Data.Sqlite.SqliteConnection db); void Process(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize); - void Finalize(SQLiteConnection db); -} \ No newline at end of file +} + +public interface ISQLiteFileParser : IDisposable +{ + void Init(SqliteConnection db); + bool CanParse(string filename); + void Parse(string filename); + public bool Verbose { get; set; } + public bool SkipReferences { get; set; } +} diff --git a/Analyzer/SQLite/Handlers/MeshHandler.cs b/Analyzer/SQLite/Handlers/MeshHandler.cs index a5c3575..20b22ee 100644 --- a/Analyzer/SQLite/Handlers/MeshHandler.cs +++ b/Analyzer/SQLite/Handlers/MeshHandler.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Data.SQLite; +using System; using System.Text; +using Microsoft.Data.Sqlite; using UnityDataTools.Analyzer.SerializedObjects; using UnityDataTools.FileSystem.TypeTreeReaders; @@ -10,33 +8,32 @@ namespace UnityDataTools.Analyzer.SQLite.Handlers; public class MeshHandler : ISQLiteHandler { - SQLiteCommand m_InsertCommand; + SqliteCommand m_InsertCommand; - public void Init(SQLiteConnection db) + public void Init(SqliteConnection db) { - using var command = new SQLiteCommand(db); - - command.CommandText = Properties.Resources.Mesh; + using var command = db.CreateCommand(); + command.CommandText = Resources.Mesh; command.ExecuteNonQuery(); - m_InsertCommand = new SQLiteCommand(db); + m_InsertCommand = db.CreateCommand(); m_InsertCommand.CommandText = "INSERT INTO meshes(id, sub_meshes, blend_shapes, bones, indices, vertices, compression, rw_enabled, vertex_size, channels) VALUES(@id, @sub_meshes, @blend_shapes, @bones, @indices, @vertices, @compression, @rw_enabled, @vertex_size, @channels)"; - m_InsertCommand.Parameters.Add("@id", DbType.Int64); - m_InsertCommand.Parameters.Add("@sub_meshes", DbType.Int32); - m_InsertCommand.Parameters.Add("@blend_shapes", DbType.Int32); - m_InsertCommand.Parameters.Add("@bones", DbType.Int32); - m_InsertCommand.Parameters.Add("@indices", DbType.Int32); - m_InsertCommand.Parameters.Add("@vertices", DbType.Int32); - m_InsertCommand.Parameters.Add("@compression", DbType.Int32); - m_InsertCommand.Parameters.Add("@rw_enabled", DbType.Int32); - m_InsertCommand.Parameters.Add("@vertex_size", DbType.Int32); - m_InsertCommand.Parameters.Add("@channels", DbType.String); + m_InsertCommand.Parameters.Add("@id", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@sub_meshes", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@blend_shapes", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@bones", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@indices", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@vertices", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@compression", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@rw_enabled", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@vertex_size", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@channels", SqliteType.Text); } public void Process(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize) { var mesh = Mesh.Read(reader); - + m_InsertCommand.Transaction = ctx.Transaction; m_InsertCommand.Parameters["@id"].Value = objectId; m_InsertCommand.Parameters["@indices"].Value = mesh.Indices; m_InsertCommand.Parameters["@vertices"].Value = mesh.Vertices; @@ -57,8 +54,8 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s channels.Append(channel.Dimension); channels.AppendLine("]"); } - - m_InsertCommand.Parameters["@channels"].Value = channels; + + m_InsertCommand.Parameters["@channels"].Value = channels.ToString(); m_InsertCommand.ExecuteNonQuery(); @@ -66,12 +63,12 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s name = mesh.Name; } - public void Finalize(SQLiteConnection db) + public void Finalize(SqliteConnection db) { } void IDisposable.Dispose() { - m_InsertCommand.Dispose(); + m_InsertCommand?.Dispose(); } -} \ No newline at end of file +} 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/Handlers/PreloadDataHandler.cs b/Analyzer/SQLite/Handlers/PreloadDataHandler.cs index 7664b61..d0c9d1e 100644 --- a/Analyzer/SQLite/Handlers/PreloadDataHandler.cs +++ b/Analyzer/SQLite/Handlers/PreloadDataHandler.cs @@ -1,6 +1,6 @@ using System; using System.Data; -using System.Data.SQLite; +using Microsoft.Data.Sqlite; using UnityDataTools.Analyzer.SerializedObjects; using UnityDataTools.FileSystem.TypeTreeReaders; @@ -8,29 +8,28 @@ namespace UnityDataTools.Analyzer.SQLite.Handlers; public class PreloadDataHandler : ISQLiteHandler { - SQLiteCommand m_InsertDepCommand; + private SqliteCommand m_InsertDepCommand; - public void Init(SQLiteConnection db) + public void Init(SqliteConnection db) { - using var command = new SQLiteCommand(db); - - m_InsertDepCommand = new SQLiteCommand(db); + m_InsertDepCommand = db.CreateCommand(); + m_InsertDepCommand.Connection = db; m_InsertDepCommand.CommandText = "INSERT INTO asset_dependencies(object, dependency) VALUES(@object, @dependency)"; - m_InsertDepCommand.Parameters.Add("@object", DbType.Int64); - m_InsertDepCommand.Parameters.Add("@dependency", DbType.Int64); + m_InsertDepCommand.Parameters.Add("@object", SqliteType.Integer); + m_InsertDepCommand.Parameters.Add("@dependency", SqliteType.Integer); } public void Process(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize) { var preloadData = PreloadData.Read(reader); - + m_InsertDepCommand.Transaction = ctx.Transaction; m_InsertDepCommand.Parameters["@object"].Value = ctx.SceneId; foreach (var asset in preloadData.Assets) { var fileId = ctx.LocalToDbFileId[asset.FileId]; var objId = ctx.ObjectIdProvider.GetId((fileId, asset.PathId)); - + m_InsertDepCommand.Parameters["@dependency"].Value = objId; m_InsertDepCommand.ExecuteNonQuery(); } @@ -39,12 +38,12 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s streamDataSize = 0; } - public void Finalize(SQLiteConnection db) + public void Finalize(SqliteConnection db) { } void IDisposable.Dispose() { - m_InsertDepCommand.Dispose(); + m_InsertDepCommand?.Dispose(); } -} \ No newline at end of file +} diff --git a/Analyzer/SQLite/Handlers/ShaderHandler.cs b/Analyzer/SQLite/Handlers/ShaderHandler.cs index c4394ef..b1eb520 100644 --- a/Analyzer/SQLite/Handlers/ShaderHandler.cs +++ b/Analyzer/SQLite/Handlers/ShaderHandler.cs @@ -1,58 +1,56 @@ -using System; +using System; using System.Collections.Generic; -using System.Data; -using System.Data.SQLite; +using Microsoft.Data.Sqlite; using UnityDataTools.FileSystem.TypeTreeReaders; namespace UnityDataTools.Analyzer.SQLite.Handlers; public class ShaderHandler : ISQLiteHandler { - SQLiteCommand m_InsertCommand; - SQLiteCommand m_InsertSubProgramCommand; - SQLiteCommand m_InsertKeywordCommand; - SQLiteCommand m_InsertSubProgramKeywordsCommand; - + private SqliteCommand m_InsertCommand; + private SqliteCommand m_InsertSubProgramCommand; + private SqliteCommand m_InsertKeywordCommand; + private SqliteCommand m_InsertSubProgramKeywordsCommand; + static long s_SubProgramId = 0; static Dictionary s_GlobalKeywords = new(); - public void Init(SQLiteConnection db) + public void Init(SqliteConnection db) { s_SubProgramId = 0; s_GlobalKeywords.Clear(); - - using var command = new SQLiteCommand(db); - command.CommandText = Properties.Resources.Shader; + using var command = db.CreateCommand(); + command.CommandText = Resources.Shader; command.ExecuteNonQuery(); - m_InsertCommand = new SQLiteCommand(db); + m_InsertCommand = db.CreateCommand(); m_InsertCommand.CommandText = "INSERT INTO shaders(id, decompressed_size, unique_programs) VALUES(@id, @decompressed_size, @unique_programs)"; - m_InsertCommand.Parameters.Add("@id", DbType.Int64); - m_InsertCommand.Parameters.Add("@decompressed_size", DbType.Int32); - m_InsertCommand.Parameters.Add("@unique_programs", DbType.Int32); + m_InsertCommand.Parameters.Add("@id", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@decompressed_size", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@unique_programs", SqliteType.Integer); - m_InsertSubProgramCommand = new SQLiteCommand(db); + m_InsertSubProgramCommand = db.CreateCommand(); m_InsertSubProgramCommand.CommandText = "INSERT INTO shader_subprograms(id, shader, sub_shader, pass, pass_name, sub_program, hw_tier, shader_type, api) VALUES(@id, @shader, @sub_shader, @pass, @pass_name, @sub_program, @hw_tier, @shader_type, @api)"; - m_InsertSubProgramCommand.Parameters.Add("@id", DbType.Int64); - m_InsertSubProgramCommand.Parameters.Add("@shader", DbType.Int64); - m_InsertSubProgramCommand.Parameters.Add("@sub_shader", DbType.Int32); - m_InsertSubProgramCommand.Parameters.Add("@pass", DbType.Int32); - m_InsertSubProgramCommand.Parameters.Add("@pass_name", DbType.String); - m_InsertSubProgramCommand.Parameters.Add("@sub_program", DbType.Int32); - m_InsertSubProgramCommand.Parameters.Add("@hw_tier", DbType.Int32); - m_InsertSubProgramCommand.Parameters.Add("@shader_type", DbType.String); - m_InsertSubProgramCommand.Parameters.Add("@api", DbType.Int32); - - m_InsertKeywordCommand = new SQLiteCommand(db); + m_InsertSubProgramCommand.Parameters.Add("@id", SqliteType.Integer); + m_InsertSubProgramCommand.Parameters.Add("@shader", SqliteType.Integer); + m_InsertSubProgramCommand.Parameters.Add("@sub_shader", SqliteType.Integer); + m_InsertSubProgramCommand.Parameters.Add("@pass", SqliteType.Integer); + m_InsertSubProgramCommand.Parameters.Add("@pass_name", SqliteType.Text); + m_InsertSubProgramCommand.Parameters.Add("@sub_program", SqliteType.Integer); + m_InsertSubProgramCommand.Parameters.Add("@hw_tier", SqliteType.Integer); + m_InsertSubProgramCommand.Parameters.Add("@shader_type", SqliteType.Text); + m_InsertSubProgramCommand.Parameters.Add("@api", SqliteType.Integer); + + m_InsertKeywordCommand = db.CreateCommand(); m_InsertKeywordCommand.CommandText = "INSERT INTO shader_keywords(id, keyword) VALUES(@id, @keyword)"; - m_InsertKeywordCommand.Parameters.Add("@id", DbType.Int32); - m_InsertKeywordCommand.Parameters.Add("@keyword", DbType.String); + m_InsertKeywordCommand.Parameters.Add("@id", SqliteType.Integer); + m_InsertKeywordCommand.Parameters.Add("@keyword", SqliteType.Text); - m_InsertSubProgramKeywordsCommand = new SQLiteCommand(db); + m_InsertSubProgramKeywordsCommand = db.CreateCommand(); m_InsertSubProgramKeywordsCommand.CommandText = "INSERT INTO shader_subprogram_keywords(subprogram_id, keyword_id) VALUES (@subprogram_id, @keyword_id)"; - m_InsertSubProgramKeywordsCommand.Parameters.Add("@subprogram_id", DbType.Int64); - m_InsertSubProgramKeywordsCommand.Parameters.Add("@keyword_id", DbType.Int32); + m_InsertSubProgramKeywordsCommand.Parameters.Add("@subprogram_id", SqliteType.Integer); + m_InsertSubProgramKeywordsCommand.Parameters.Add("@keyword_id", SqliteType.Integer); } public void Process(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize) @@ -64,10 +62,11 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s for (int i = 0; i < shader.Keywords.Count; ++i) { var keyword = shader.Keywords[i]; - var id = GetKeywordId(keyword); + var id = GetKeywordId(keyword, ctx.Transaction); localToGlobalKeywords[i] = id; } + m_InsertCommand.Transaction = ctx.Transaction; m_InsertCommand.Parameters["@id"].Value = objectId; for (int subShaderIndex = 0; subShaderIndex < shader.SubShaders.Count; ++subShaderIndex) @@ -79,7 +78,7 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s for (int passIndex = 0; passIndex < subShader.Passes.Count; ++passIndex) { var pass = subShader.Passes[passIndex]; - + m_InsertSubProgramCommand.Transaction = ctx.Transaction; m_InsertSubProgramCommand.Parameters["@shader"].Value = objectId; m_InsertSubProgramCommand.Parameters["@pass"].Value = passIndex; m_InsertSubProgramCommand.Parameters["@pass_name"].Value = pass.Name; @@ -88,7 +87,7 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s { var progType = kv.Key; var programs = kv.Value; - + m_InsertSubProgramCommand.Parameters["@shader_type"].Value = progType; for (int programIndex = 0; programIndex < programs.Count; ++programIndex) @@ -96,13 +95,14 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s var program = programs[programIndex]; uniquePrograms.Add(program.BlobIndex); - + m_InsertSubProgramCommand.Parameters["@id"].Value = s_SubProgramId; m_InsertSubProgramCommand.Parameters["@sub_program"].Value = programIndex; m_InsertSubProgramCommand.Parameters["@hw_tier"].Value = program.HwTier; m_InsertSubProgramCommand.Parameters["@api"].Value = program.Api; m_InsertSubProgramCommand.ExecuteNonQuery(); + m_InsertSubProgramKeywordsCommand.Transaction = ctx.Transaction; m_InsertSubProgramKeywordsCommand.Parameters["@subprogram_id"].Value = s_SubProgramId; foreach (var keyword in program.Keywords) { @@ -115,18 +115,18 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s } } } - + m_InsertCommand.Parameters["@id"].Value = objectId; m_InsertCommand.Parameters["@decompressed_size"].Value = shader.DecompressedSize; m_InsertCommand.Parameters["@unique_programs"].Value = uniquePrograms.Count; - + m_InsertCommand.ExecuteNonQuery(); name = shader.Name; streamDataSize = 0; } - private int GetKeywordId(string keyword) + private int GetKeywordId(string keyword, SqliteTransaction ctxTransaction) { int id; @@ -134,7 +134,7 @@ private int GetKeywordId(string keyword) { id = s_GlobalKeywords.Count; s_GlobalKeywords[keyword] = id; - + m_InsertKeywordCommand.Transaction = ctxTransaction; m_InsertKeywordCommand.Parameters["@id"].Value = id; m_InsertKeywordCommand.Parameters["@keyword"].Value = keyword; m_InsertKeywordCommand.ExecuteNonQuery(); @@ -143,22 +143,22 @@ private int GetKeywordId(string keyword) return id; } - public void Finalize(SQLiteConnection db) + public void Finalize(SqliteConnection db) { - using var command = new SQLiteCommand(db); - + using var command = new SqliteCommand(); + command.Connection = db; command.CommandText = "CREATE INDEX shader_subprograms_shader_index ON shader_subprograms(shader)"; command.ExecuteNonQuery(); - + command.CommandText = "CREATE INDEX shader_subprogram_keywords_subprogram_id_index ON shader_subprogram_keywords(subprogram_id)"; command.ExecuteNonQuery(); } void IDisposable.Dispose() { - m_InsertCommand.Dispose(); - m_InsertSubProgramCommand.Dispose(); - m_InsertKeywordCommand.Dispose(); - m_InsertSubProgramKeywordsCommand.Dispose(); + m_InsertCommand?.Dispose(); + m_InsertSubProgramCommand?.Dispose(); + m_InsertKeywordCommand?.Dispose(); + m_InsertSubProgramKeywordsCommand?.Dispose(); } -} \ No newline at end of file +} diff --git a/Analyzer/SQLite/Handlers/Texture2DHandler.cs b/Analyzer/SQLite/Handlers/Texture2DHandler.cs index dfa9103..5cdb6d9 100644 --- a/Analyzer/SQLite/Handlers/Texture2DHandler.cs +++ b/Analyzer/SQLite/Handlers/Texture2DHandler.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Data.SQLite; +using System; +using Microsoft.Data.Sqlite; using UnityDataTools.Analyzer.SerializedObjects; using UnityDataTools.FileSystem.TypeTreeReaders; @@ -9,48 +7,46 @@ namespace UnityDataTools.Analyzer.SQLite.Handlers; public class Texture2DHandler : ISQLiteHandler { - SQLiteCommand m_InsertCommand; + SqliteCommand m_InsertCommand = new SqliteCommand(); - public void Init(SQLiteConnection db) + public void Init(SqliteConnection db) { - using var command = new SQLiteCommand(db); - - command.CommandText = Properties.Resources.Texture2D; + using var command = db.CreateCommand(); + command.CommandText = Resources.Texture2D; command.ExecuteNonQuery(); - m_InsertCommand = new SQLiteCommand(db); + m_InsertCommand = db.CreateCommand(); m_InsertCommand.CommandText = "INSERT INTO textures(id, width, height, format, rw_enabled, mip_count) VALUES(@id, @width, @height, @format, @rw_enabled, @mip_count)"; - m_InsertCommand.Parameters.Add("@id", DbType.Int64); - m_InsertCommand.Parameters.Add("@width", DbType.Int32); - m_InsertCommand.Parameters.Add("@height", DbType.Int32); - m_InsertCommand.Parameters.Add("@format", DbType.Int32); - m_InsertCommand.Parameters.Add("@rw_enabled", DbType.Int32); - m_InsertCommand.Parameters.Add("@mip_count", DbType.Int32); + m_InsertCommand.Parameters.Add("@id", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@width", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@height", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@format", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@rw_enabled", SqliteType.Integer); + m_InsertCommand.Parameters.Add("@mip_count", SqliteType.Integer); } public void Process(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize) { var texture2d = Texture2D.Read(reader); - + m_InsertCommand.Transaction = ctx.Transaction; m_InsertCommand.Parameters["@id"].Value = objectId; m_InsertCommand.Parameters["@width"].Value = texture2d.Width; m_InsertCommand.Parameters["@height"].Value = texture2d.Height; m_InsertCommand.Parameters["@format"].Value = texture2d.Format; m_InsertCommand.Parameters["@rw_enabled"].Value = texture2d.RwEnabled; m_InsertCommand.Parameters["@mip_count"].Value = texture2d.MipCount; - m_InsertCommand.ExecuteNonQuery(); name = texture2d.Name; streamDataSize = texture2d.StreamDataSize; } - public void Finalize(SQLiteConnection db) + public void Finalize(SqliteConnection db) { } void IDisposable.Dispose() { - m_InsertCommand.Dispose(); + m_InsertCommand?.Dispose(); } -} \ No newline at end of file +} diff --git a/Analyzer/SQLite/Parsers/AddressablesBuildLayoutParser.cs b/Analyzer/SQLite/Parsers/AddressablesBuildLayoutParser.cs new file mode 100644 index 0000000..9907f33 --- /dev/null +++ b/Analyzer/SQLite/Parsers/AddressablesBuildLayoutParser.cs @@ -0,0 +1,86 @@ +using System; +using System.IO; +using Microsoft.Data.Sqlite; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using UnityDataTools.Analyzer.SQLite.Handlers; +using UnityDataTools.Analyzer.SQLite.Parsers.Models; +using UnityDataTools.Analyzer.SQLite.Writers; + +namespace UnityDataTools.Analyzer.SQLite.Parsers +{ + public class AddressablesBuildLayoutParser : ISQLiteFileParser + { + private AddressablesBuildLayoutSQLWriter m_Writer; + + public bool Verbose { get; set; } + public bool SkipReferences { get; set; } + + public void Dispose() + { + m_Writer.Dispose(); + } + public void Init(SqliteConnection db) + { + m_Writer = new AddressablesBuildLayoutSQLWriter(db); + m_Writer.Verbose = Verbose; + } + + public bool CanParse(string filename) + { + + if (Path.GetExtension(filename) != ".json") + return false; + + // Read the first line of the JSON file and check if it contains BuildResultHash + string firstLine = ""; + try + { + using (StreamReader reader = new StreamReader(filename)) + { + firstLine = reader.ReadLine(); + if (firstLine != null) + { + // Remove trailing comma if present and add closing brace to make it valid JSON + if (firstLine.TrimEnd().EndsWith(",")) + { + firstLine = firstLine.TrimEnd().TrimEnd(',') + "}"; + } + + using (JsonTextReader jsonReader = new JsonTextReader(new StringReader(firstLine))) + { + JsonSerializer serializer = new JsonSerializer(); + var jsonObject = serializer.Deserialize(jsonReader); + + // If the file has BuildResultHash, process it as an Addressables build + if (jsonObject != null && jsonObject["BuildResultHash"] != null) + { + return true; + } + } + } + } + } + catch (Exception e) + { + if (Verbose) + { + Console.Error.WriteLine($"Error reading JSON file {filename}: {e.Message}"); + } + } + return false; + } + + public void Parse(string filename) + { + // only init our writer if we are actually parsing a file + m_Writer.Init(); + using (StreamReader reader = File.OpenText(filename)) + { + JsonSerializer serializer = new JsonSerializer(); + BuildLayout buildLayout = (BuildLayout)serializer.Deserialize(reader, typeof(BuildLayout)); + m_Writer.WriteAddressablesBuild(filename, buildLayout); + } + } + } +} diff --git a/Analyzer/SQLite/Parsers/Models/BuildLayout.cs b/Analyzer/SQLite/Parsers/Models/BuildLayout.cs new file mode 100644 index 0000000..16d7781 --- /dev/null +++ b/Analyzer/SQLite/Parsers/Models/BuildLayout.cs @@ -0,0 +1,194 @@ +using System.Collections.Generic; + +// this file comes from Addressables and is used to serialize and deserialize +// build layout information to JSON +namespace UnityDataTools.Analyzer.SQLite.Parsers.Models +{ + public class BuildLayout + { + /// + /// Build Platform Addressables build is targeting + /// + public int BuildTarget; + + /// + /// Hash of the build results + /// + public string BuildResultHash; + + /// + /// If the build was a new build or an update for a previous build + /// + public int BuildType; + + /// + /// DateTime at the start of building Addressables + /// + public string BuildStartTime; + + /// + /// Time in seconds taken to build Addressables Content + /// + public double Duration; + + /// + /// Null or Empty if the build completed successfully, else contains error causing the failure + /// + public string BuildError; + + /// + /// Version of the Unity editor used to perform the build. + /// + public string UnityVersion; + + /// + /// Version of the Addressables package used to perform the build. + /// + public string PackageVersion; + + /// + /// Player build version for the build, this is a timestamp if PlayerVersionOverride is not set in the settings + /// + public string PlayerBuildVersion; + + /// + /// Name of the build script to build + /// + public string BuildScript; + + public References references; + + } + + public class References + { + public List RefIds = new(); + } + + public class ReferenceId + { + public int rid; + } + + public class Reference : ReferenceId + { + public ReferenceData data; + + public ReferenceType type; + } + + // This is a grab bag of data because Unity's built-in serialization system + // (used for MonoBehaviours, ScriptableObjects, etc.) does not support polymorphism. + // To maintain compatibility, all possible fields are listed in the ReferenceData class. + public class ReferenceData + { + public ReferenceId Bundle; + public ReferenceId File; + public AssetHash AssetHash; + public string AssetPath; + public string AddressableName; + public string AddressableGuid; + public ReferenceId[] ExternallyReferencedAssets; + public string GroupGuid; + public string Guid; + public string InternalId; + public ReferenceId[] InternalReferencedExplicitAssets; + public ReferenceId[] InternalReferencedOtherAssets; + public string[] Labels; + public int MainAssetType; + public int StreamedSize; + public int SerializedSize; + public string Name; + public string CRC; + + // For BuildLayout/Bundle + public int AssetCount; + public int BuildStatus; + public ReferenceId[] BundleDependencies; + public string Compression; + public ReferenceId[] Dependencies; + public int DependencyFileSize; + public ReferenceId[] DependentBundles; + public ReferenceId[] ExpandedDependencies; + public int ExpandedDependencyFileSize; + public int FileSize; + public ReferenceId[] Files; + public ReferenceId Group; + public object Hash; // Complex object containing Hash and serializedVersion + public string InternalName; + public string LoadPath; + public string Provider; + public string ResultType; + + // For BuildLayout/DataFromOtherAsset + public string AssetGuid; + public int ObjectCount; + public AssetObject[] Objects; // Array of object data + public ReferenceId[] ReferencingAssets; + + // For BuildLayout/File + public ReferenceId[] Assets; + public BundleObjectInfo BundleObjectInfo; // Object with Size property + public ReferenceId[] ExternalReferences; + public int MonoScriptCount; + public int MonoScriptSize; + public ReferenceId[] OtherAssets; + public int PreloadInfoSize; + public ReferenceId[] SubFiles; + public string WriteResultFilename; + + // For BuildLayout/Group + public ReferenceId[] Bundles; + public string PackingMode; + public ReferenceId[] Schemas; + + // For BuildLayout/SchemaData + public SchemaDataPair[] SchemaDataPairs; // Array of Key-Value pairs + public string Type; + + // For BuildLayout/SubFile + public bool IsSerializedFile; + public int Size; + } + + public class ReferenceType + { + public string Asm; + public string Class; + public string NS; + } + + public class AssetHash + { + public string Hash; + public string serializedVersion; + } + + public class BundleObjectInfo + { + public int Size; + } + + public class AssetObject + { + public int AssetType; + public string ComponentName; + public long LocalIdentifierInFile; + public string ObjectName; + public int SerializedSize; + public ObjectReference[] References; // Array of object references + public int StreamedSize; + } + + public class ObjectReference + { + public int AssetId; + public int ObjectId; + } + + public class SchemaDataPair + { + public string Key; + public string Value; + } +} diff --git a/Analyzer/SQLite/Parsers/SerializedFileParser.cs b/Analyzer/SQLite/Parsers/SerializedFileParser.cs new file mode 100644 index 0000000..acd1658 --- /dev/null +++ b/Analyzer/SQLite/Parsers/SerializedFileParser.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Data.Sqlite; +using UnityDataTools.Analyzer.SQLite.Handlers; +using UnityDataTools.Analyzer.SQLite.Writers; +using UnityDataTools.FileSystem; + +namespace UnityDataTools.Analyzer.SQLite.Parsers +{ + public class SerializedFileParser : ISQLiteFileParser + { + private SerializedFileSQLiteWriter m_Writer; + + public bool Verbose { get; set; } + public bool SkipReferences { get; set; } + + public bool CanParse(string filename) + { + return ShouldIgnoreFile(filename) == false; + } + + + public void Dispose() + { + m_Writer.Dispose(); + } + + public void Init(SqliteConnection db) + { + m_Writer = new SerializedFileSQLiteWriter(db, SkipReferences); + } + + public void Parse(string filename) + { + // only init our writer if we are actually parsing a file + m_Writer.Init(); + ProcessFile(filename, Path.GetDirectoryName(filename)); + } + + bool ShouldIgnoreFile(string file) + { + // Unfortunately there is no standard extension for AssetBundles, and SerializedFiles often have no extension at all. + // Also there is also no distinctive signature at the start of a SerializedFile to immediately recognize it based on its first bytes. + // This makes it difficult to use the "--search-pattern" argument to only pick those files. + + // Hence to reduce noise in UnityDataTool output we filter out files that we have a high confidence are + // NOT SerializedFiles or Unity Archives. + + string fileName = Path.GetFileName(file); + string extension = Path.GetExtension(file); + + return IgnoredFileNames.Contains(fileName) || IgnoredExtensions.Contains(extension); + } + + // These lists are based on expected output files in Player, AssetBundle, Addressables and ECS builds. + // However this is by no means exhaustive. + private static readonly HashSet IgnoredFileNames = new() + { + ".DS_Store", "boot.config", "archive_dependencies.bin", "scene_info.bin", "app.info", "link.xml", + "catalog.bin", "catalog.hash" + }; + + private static readonly HashSet IgnoredExtensions = new() + { + ".txt", ".resS", ".resource", ".json", ".dll", ".pdb", ".exe", ".manifest", ".entities", ".entityheader", + ".ini", ".config", ".hash", ".md" + }; + + bool ProcessFile(string file, string rootDirectory) + { + bool successful = true; + try + { + if (IsUnityArchive(file)) + { + using (UnityArchive archive = UnityFileSystem.MountArchive(file, "archive:" + Path.DirectorySeparatorChar)) + { + if (archive == null) + throw new FileLoadException($"Failed to mount archive: {file}"); + + try + { + var assetBundleName = Path.GetRelativePath(rootDirectory, file); + + m_Writer.BeginAssetBundle(assetBundleName, new FileInfo(file).Length); + + foreach (var node in archive.Nodes) + { + if (node.Flags.HasFlag(ArchiveNodeFlags.SerializedFile)) + { + try + { + m_Writer.WriteSerializedFile(node.Path, "archive:/" + node.Path, Path.GetDirectoryName(file)); + } + catch (Exception e) + { + // the most likely exception here is Microsoft.Data.Sqlite.SqliteException, + // for example 'UNIQUE constraint failed: serialized_files.id'. + // or 'UNIQUE constraint failed: objects.id' which can happen + // if AssetBundles from different builds are being processed by a single call to Analyze + // or if there is a Unity Data Tool bug. + Console.Error.WriteLine($"Error processing {node.Path} in archive {file}"); + Console.Error.WriteLine(e.Message); + Console.WriteLine(); + + // It is possible some files inside an archive will pass and others will fail, to have a partial analyze. + // Overall that is reported as a failure + successful = false; + } + } + } + } + finally + { + m_Writer.EndAssetBundle(); + } + } + } + else + { + // This isn't a Unity Archive file. Try to open it as a SerializedFile. + // Unfortunately there is no standard file extension, or clear signature at the start of the file, + // to test if it truly is a SerializedFile. So this will process files that are clearly not unity build files, + // and there is a chance for crashes and freezes if the parser misinterprets the file content. + var relativePath = Path.GetRelativePath(rootDirectory, file); + m_Writer.WriteSerializedFile(relativePath, file, Path.GetDirectoryName(file)); + } + } + catch (NotSupportedException) + { + Console.Error.WriteLine(); + //A "failed to load" error will already be logged by the UnityFileSystem library + + successful = false; + } + catch (Exception e) + { + Console.Error.WriteLine(); + Console.Error.WriteLine($"Error processing file: {file}"); + Console.WriteLine($"{e.GetType()}: {e.Message}"); + if (Verbose) + Console.WriteLine(e.StackTrace); + + successful = false; + } + + return successful; + } + + private static bool IsUnityArchive(string filePath) + { + // Check whether a file is a Unity Archive (AssetBundle) by looking for known signatures at the start of the file. + // "UnifyFS" is the current signature, but some older formats of the file are still supported + string[] signatures = { "UnityFS", "UnityWeb", "UnityRaw", "UnityArchive" }; + int maxLen = 12; // "UnityArchive".Length + byte[] buffer = new byte[maxLen]; + + using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + int read = fs.Read(buffer, 0, buffer.Length); + foreach (var sig in signatures) + { + if (read >= sig.Length) + { + bool match = true; + for (int i = 0; i < sig.Length; ++i) + { + if (buffer[i] != sig[i]) + { + match = false; + break; + } + } + if (match) + return true; + } + } + return false; + } + } + } +} diff --git a/Analyzer/SQLite/SQLiteWriter.cs b/Analyzer/SQLite/SQLiteWriter.cs deleted file mode 100644 index b65b3bc..0000000 --- a/Analyzer/SQLite/SQLiteWriter.cs +++ /dev/null @@ -1,336 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Data.SQLite; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Text.RegularExpressions; -using UnityDataTools.Analyzer.SerializedObjects; -using UnityDataTools.Analyzer.SQLite.Handlers; -using UnityDataTools.FileSystem; -using UnityDataTools.FileSystem.TypeTreeReaders; - -namespace UnityDataTools.Analyzer.SQLite; - -public class SQLiteWriter : IWriter -{ - private HashSet m_TypeSet = new (); - - private int m_CurrentAssetBundleId = -1; - private int m_NextAssetBundleId = 0; - - private string m_DatabaseName; - private bool m_SkipReferences; - - private Util.IdProvider m_SerializedFileIdProvider = new (); - private Util.ObjectIdProvider m_ObjectIdProvider = new (); - - private Regex m_RegexSceneFile = new(@"BuildPlayer-([^\.]+)(?:\.sharedAssets)?"); - - // Used to map PPtr fileId to its corresponding serialized file id in the database. - Dictionary m_LocalToDbFileId = new (); - - private Dictionary m_Handlers = new () - { - { "Mesh", new MeshHandler() }, - { "Texture2D", new Texture2DHandler() }, - { "Shader", new ShaderHandler() }, - { "AudioClip", new AudioClipHandler() }, - { "AnimationClip", new AnimationClipHandler() }, - { "AssetBundle", new AssetBundleHandler() }, - { "PreloadData", new PreloadDataHandler() }, - }; - - private SQLiteConnection m_Database; - private SQLiteCommand m_AddReferenceCommand; - private SQLiteCommand m_AddAssetBundleCommand; - private SQLiteCommand m_AddSerializedFileCommand; - private SQLiteCommand m_AddObjectCommand; - private SQLiteCommand m_AddTypeCommand; - private SQLiteCommand m_InsertDepCommand; - - public SQLiteWriter(string databaseName, bool skipReferences) - { - m_DatabaseName = databaseName; - m_SkipReferences = skipReferences; - } - - public void Begin() - { - if (m_Database != null) - { - throw new InvalidOperationException("SQLiteWriter.Begin called twice"); - } - - m_Database = new SQLiteConnection($"Data Source={m_DatabaseName};Version=3;New=True;Foreign Keys=False;"); - - SQLiteConnection.CreateFile(m_DatabaseName); - m_Database.Open(); - - using var command = m_Database.CreateCommand(); - command.CommandText = Properties.Resources.Init; - command.ExecuteNonQuery(); - - foreach (var handler in m_Handlers.Values) - { - handler.Init(m_Database); - } - - CreateSQLiteCommands(); - } - - public void End() - { - if (m_Database == null) - { - throw new InvalidOperationException("SQLiteWriter.End called before SQLiteWriter.Begin"); - } - - foreach (var handler in m_Handlers.Values) - { - handler.Finalize(m_Database); - } - - using var finalizeCommand = m_Database.CreateCommand(); - finalizeCommand.CommandText = Properties.Resources.Finalize; - finalizeCommand.ExecuteNonQuery(); - } - - private void CreateSQLiteCommands() - { - m_AddAssetBundleCommand = m_Database.CreateCommand(); - m_AddAssetBundleCommand.CommandText = "INSERT INTO asset_bundles (id, name, file_size) VALUES (@id, @name, @file_size)"; - m_AddAssetBundleCommand.Parameters.Add("@id", DbType.Int32); - m_AddAssetBundleCommand.Parameters.Add("@name", DbType.String); - m_AddAssetBundleCommand.Parameters.Add("@file_size", DbType.Int64); - - m_AddSerializedFileCommand = m_Database.CreateCommand(); - m_AddSerializedFileCommand.CommandText = "INSERT INTO serialized_files (id, asset_bundle, name) VALUES (@id, @asset_bundle, @name)"; - m_AddSerializedFileCommand.Parameters.Add("@id", DbType.Int32); - m_AddSerializedFileCommand.Parameters.Add("@asset_bundle", DbType.Int32); - m_AddSerializedFileCommand.Parameters.Add("@name", DbType.String); - - m_AddReferenceCommand = m_Database.CreateCommand(); - m_AddReferenceCommand.CommandText = "INSERT INTO refs (object, referenced_object, property_path, property_type) VALUES (@object, @referenced_object, @property_path, @property_type)"; - m_AddReferenceCommand.Parameters.Add("@object", DbType.Int64); - m_AddReferenceCommand.Parameters.Add("@referenced_object", DbType.Int64); - m_AddReferenceCommand.Parameters.Add("@property_path", DbType.String); - m_AddReferenceCommand.Parameters.Add("@property_type", DbType.String); - - m_AddObjectCommand = m_Database.CreateCommand(); - m_AddObjectCommand.CommandText = "INSERT INTO objects (id, object_id, serialized_file, type, name, game_object, size, crc32) VALUES (@id, @object_id, @serialized_file, @type, @name, @game_object, @size, @crc32)"; - m_AddObjectCommand.Parameters.Add("@id", DbType.Int64); - m_AddObjectCommand.Parameters.Add("@object_id", DbType.Int64); - m_AddObjectCommand.Parameters.Add("@serialized_file", DbType.Int32); - m_AddObjectCommand.Parameters.Add("@type", DbType.Int32); - m_AddObjectCommand.Parameters.Add("@name", DbType.String); - m_AddObjectCommand.Parameters.Add("@game_object", DbType.Int64); - m_AddObjectCommand.Parameters.Add("@size", DbType.Int64); - m_AddObjectCommand.Parameters.Add("@crc32", DbType.UInt32); - - m_AddTypeCommand = m_Database.CreateCommand(); - m_AddTypeCommand.CommandText = "INSERT INTO types (id, name) VALUES (@id, @name)"; - m_AddTypeCommand.Parameters.Add("@id", DbType.Int32); - m_AddTypeCommand.Parameters.Add("@name", DbType.String); - - m_InsertDepCommand = m_Database.CreateCommand(); - m_InsertDepCommand.CommandText = "INSERT INTO asset_dependencies(object, dependency) VALUES(@object, @dependency)"; - m_InsertDepCommand.Parameters.Add("@object", DbType.Int64); - m_InsertDepCommand.Parameters.Add("@dependency", DbType.Int64); - } - - public void BeginAssetBundle(string name, long size) - { - if (m_CurrentAssetBundleId != -1) - { - throw new InvalidOperationException("SQLWriter.BeginAssetBundle called twice"); - } - - m_CurrentAssetBundleId = m_NextAssetBundleId++; - m_AddAssetBundleCommand.Parameters["@id"].Value = m_CurrentAssetBundleId; - m_AddAssetBundleCommand.Parameters["@name"].Value = name; - m_AddAssetBundleCommand.Parameters["@file_size"].Value = size; - m_AddAssetBundleCommand.ExecuteNonQuery(); - } - - public void EndAssetBundle() - { - if (m_CurrentAssetBundleId == -1) - { - throw new InvalidOperationException("SQLWriter.EndAssetBundle called before SQLWriter.BeginAssetBundle"); - } - - m_CurrentAssetBundleId = -1; - } - - public void WriteSerializedFile(string relativePath, string fullPath, string containingFolder) - { - using var sf = UnityFileSystem.OpenSerializedFile(fullPath); - using var reader = new UnityFileReader(fullPath, 64 * 1024 * 1024); - using var pptrReader = new PPtrAndCrcProcessor(sf, reader, containingFolder, AddReference); - int serializedFileId = m_SerializedFileIdProvider.GetId(Path.GetFileName(fullPath).ToLower()); - int sceneId = -1; - - var match = m_RegexSceneFile.Match(relativePath); - - if (match.Success) - { - var sceneName = match.Groups[1].Value; - - // There is no Scene object in Unity (a Scene is the full content of a - // SerializedFile). We generate an object id using the name of the Scene - // as SerializedFile name, and the object id 0. - sceneId = m_ObjectIdProvider.GetId((m_SerializedFileIdProvider.GetId(sceneName), 0)); - - // There are 2 SerializedFiles per Scene, one ends with .sharedAssets. This is a - // dirty trick to avoid inserting the scene object a second time. - if (relativePath.EndsWith(".sharedAssets")) - { - m_AddObjectCommand.Parameters["@id"].Value = sceneId; - m_AddObjectCommand.Parameters["@object_id"].Value = 0; - m_AddObjectCommand.Parameters["@serialized_file"].Value = serializedFileId; - // The type is set to -1 which doesn't exist in Unity, but is associated to - // "Scene" in the database. - m_AddObjectCommand.Parameters["@type"].Value = -1; - m_AddObjectCommand.Parameters["@name"].Value = sceneName; - m_AddObjectCommand.Parameters["@size"].Value = 0; - m_AddObjectCommand.Parameters["@crc32"].Value = 0; - m_AddObjectCommand.ExecuteNonQuery(); - } - } - - m_LocalToDbFileId.Clear(); - - Context ctx = new() - { - AssetBundleId = m_CurrentAssetBundleId, - SerializedFileId = serializedFileId, - SceneId = sceneId, - ObjectIdProvider = m_ObjectIdProvider, - SerializedFileIdProvider = m_SerializedFileIdProvider, - LocalToDbFileId = m_LocalToDbFileId, - }; - - using var transaction = m_Database.BeginTransaction(); - - try - { - m_AddSerializedFileCommand.Parameters["@id"].Value = serializedFileId; - m_AddSerializedFileCommand.Parameters["@asset_bundle"].Value = m_CurrentAssetBundleId == -1 ? null : m_CurrentAssetBundleId; - m_AddSerializedFileCommand.Parameters["@name"].Value = relativePath; - m_AddSerializedFileCommand.ExecuteNonQuery(); - - int localId = 0; - m_LocalToDbFileId.Add(localId++, serializedFileId); - foreach (var extRef in sf.ExternalReferences) - { - m_LocalToDbFileId.Add(localId++, - m_SerializedFileIdProvider.GetId(extRef.Path.Substring(extRef.Path.LastIndexOf('/') + 1).ToLower())); - } - - foreach (var obj in sf.Objects) - { - var currentObjectId = m_ObjectIdProvider.GetId((serializedFileId, obj.Id)); - - var root = sf.GetTypeTreeRoot(obj.Id); - var offset = obj.Offset; - uint crc32 = 0; - - if (!m_TypeSet.Contains(obj.TypeId)) - { - m_AddTypeCommand.Parameters["@id"].Value = obj.TypeId; - m_AddTypeCommand.Parameters["@name"].Value = root.Type; - m_AddTypeCommand.ExecuteNonQuery(); - - m_TypeSet.Add(obj.TypeId); - } - - var randomAccessReader = new RandomAccessReader(sf, root, reader, offset); - - string name = null; - long streamDataSize = 0; - - if (m_Handlers.TryGetValue(root.Type, out var handler)) - { - handler.Process(ctx, currentObjectId, randomAccessReader, - out name, out streamDataSize); - } - else if (randomAccessReader.HasChild("m_Name")) - { - name = randomAccessReader["m_Name"].GetValue(); - } - - if (randomAccessReader.HasChild("m_GameObject")) - { - var pptr = randomAccessReader["m_GameObject"]; - var fileId = m_LocalToDbFileId[pptr["m_FileID"].GetValue()]; - m_AddObjectCommand.Parameters["@game_object"].Value = - m_ObjectIdProvider.GetId((fileId, pptr["m_PathID"].GetValue())); - } - else - { - m_AddObjectCommand.Parameters["@game_object"].Value = null; - } - - if (!m_SkipReferences) - { - crc32 = pptrReader.Process(currentObjectId, offset, root); - } - - m_AddObjectCommand.Parameters["@id"].Value = currentObjectId; - m_AddObjectCommand.Parameters["@object_id"].Value = obj.Id; - m_AddObjectCommand.Parameters["@serialized_file"].Value = serializedFileId; - m_AddObjectCommand.Parameters["@type"].Value = obj.TypeId; - m_AddObjectCommand.Parameters["@name"].Value = name; - m_AddObjectCommand.Parameters["@size"].Value = obj.Size + streamDataSize; - m_AddObjectCommand.Parameters["@crc32"].Value = crc32; - m_AddObjectCommand.ExecuteNonQuery(); - - // If this is a Scene AssetBundle, add the object as a depencency of the - // current scene. - if (ctx.SceneId != -1) - { - m_InsertDepCommand.Parameters["@object"].Value = ctx.SceneId; - m_InsertDepCommand.Parameters["@dependency"].Value = currentObjectId; - m_InsertDepCommand.ExecuteNonQuery(); - } - } - - transaction.Commit(); - } - catch (Exception) - { - transaction.Rollback(); - throw; - } - } - - private int AddReference(long objectId, int fileId, long pathId, string propertyPath, string propertyType) - { - var referencedObjectId = m_ObjectIdProvider.GetId((m_LocalToDbFileId[fileId], pathId)); - m_AddReferenceCommand.Parameters["@object"].Value = objectId; - m_AddReferenceCommand.Parameters["@referenced_object"].Value = referencedObjectId; - m_AddReferenceCommand.Parameters["@property_path"].Value = propertyPath; - m_AddReferenceCommand.Parameters["@property_type"].Value = propertyType; - m_AddReferenceCommand.ExecuteNonQuery(); - - return referencedObjectId; - } - - public void Dispose() - { - foreach (var handler in m_Handlers.Values) - { - handler.Dispose(); - } - - m_AddAssetBundleCommand.Dispose(); - m_AddSerializedFileCommand.Dispose(); - m_AddReferenceCommand.Dispose(); - m_AddObjectCommand.Dispose(); - m_AddTypeCommand.Dispose(); - m_InsertDepCommand.Dispose(); - - m_Database.Dispose(); - } -} diff --git a/Analyzer/SQLite/Writers/AddressablesBuildLayoutSQLWriter.cs b/Analyzer/SQLite/Writers/AddressablesBuildLayoutSQLWriter.cs new file mode 100644 index 0000000..a497a03 --- /dev/null +++ b/Analyzer/SQLite/Writers/AddressablesBuildLayoutSQLWriter.cs @@ -0,0 +1,585 @@ +using System; +using System.IO; +using Microsoft.Data.Sqlite; +using Newtonsoft.Json; +using UnityDataTools.Analyzer.SQLite.Commands.AddressablesBuildReport; +using UnityDataTools.Analyzer.SQLite.Parsers.Models; + +namespace UnityDataTools.Analyzer.SQLite.Writers +{ + internal class AddressablesBuildLayoutSQLWriter : IDisposable + { + private AddressablesBuild m_AddressablesBuild = new AddressablesBuild(); + private AddressablesBuildBundle m_AddressablesBuildBundle = new AddressablesBuildBundle(); + private AddressablesBuildBundleDependency m_AddressablesBuildBundleDependency = new AddressablesBuildBundleDependency(); + private AddressablesBuildBundleExpandedDependency m_AddressablesBuildBundleExpandedDependency = new AddressablesBuildBundleExpandedDependency(); + private AddressablesBuildBundleRegularDependency m_AddressablesBuildBundleRegularDependency = new AddressablesBuildBundleRegularDependency(); + private AddressablesBuildBundleDependentBundle m_AddressablesBuildBundleDependentBundle = new AddressablesBuildBundleDependentBundle(); + private AddressablesBuildBundleFile m_AddressablesBuildBundleFile = new AddressablesBuildBundleFile(); + + private AddressablesBuildDataFromOtherAsset m_AddressablesDataFromOtherAsset = new AddressablesBuildDataFromOtherAsset(); + private AddressablesBuildDataFromOtherAssetObject m_AddressablesBuildDataFromOtherAssetObject = new AddressablesBuildDataFromOtherAssetObject(); + private AddressablesBuildDataFromOtherAssetObjectReference m_AddressablesBuildDataFromOtherAssetObjectReference = new AddressablesBuildDataFromOtherAssetObjectReference(); + private AddressablesBuildDataFromOtherAssetReferencingAsset m_AddressablesBuildDataFromOtherAssetReferencingAsset = new AddressablesBuildDataFromOtherAssetReferencingAsset(); + + private AddressablesBuildExplicitAsset m_AddressablesExplicitAsset = new AddressablesBuildExplicitAsset(); + private AddressablesBuildExplicitAssetExternallyReferencedAsset m_AddressablesBuildExplicitAssetExternallyReferencedAsset = new AddressablesBuildExplicitAssetExternallyReferencedAsset(); + private AddressablesBuildExplicitAssetInternalReferencedExplicitAsset m_AddressablesBuildExplicitAssetInternalReferencedExplicitAsset = new AddressablesBuildExplicitAssetInternalReferencedExplicitAsset(); + private AddressablesBuildExplicitAssetInternalReferencedOtherAsset m_AddressablesBuildExplicitAssetInternalReferencedOtherAsset = new AddressablesBuildExplicitAssetInternalReferencedOtherAsset(); + private AddressablesBuildExplicitAssetLabel m_AddressablesBuildExplicitAssetLabel = new AddressablesBuildExplicitAssetLabel(); + + private AddressablesBuildFile m_AddressablesBuildFile = new AddressablesBuildFile(); + private AddressablesBuildFileAsset m_AddressablesBuildFileAsset = new AddressablesBuildFileAsset(); + private AddressablesBuildFileExternalReference m_AddressablesBuildFileExternalReference = new AddressablesBuildFileExternalReference(); + private AddressablesBuildFileOtherAsset m_AddressablesBuildFileOtherAsset = new AddressablesBuildFileOtherAsset(); + private AddressablesBuildFileSubFile m_AddressablesBuildFileSubFile = new AddressablesBuildFileSubFile(); + + private AddressablesBuildGroup m_AddressablesBuildGroup = new AddressablesBuildGroup(); + private AddressablesBuildGroupBundle m_AddressablesBuildGroupBundle = new AddressablesBuildGroupBundle(); + private AddressablesBuildGroupSchema m_AddressablesBuildGroupSchema = new AddressablesBuildGroupSchema(); + + private AddressablesBuildSchema m_AddressablesBuildSchema = new AddressablesBuildSchema(); + private AddressablesBuildSchemaDataPair m_AddressablesBuildSchemaDataPair = new AddressablesBuildSchemaDataPair(); + + private AddressablesBuildSubFile m_AddressablesBuildSubFile = new AddressablesBuildSubFile(); + + private SqliteCommand m_LastId = new SqliteCommand(); + + private bool m_Initialized; + private SqliteConnection m_Database; + public bool Verbose { get; set; } + + public AddressablesBuildLayoutSQLWriter(SqliteConnection database) + { + m_Initialized = false; + m_Database = database; + } + + public void Init() + { + if (m_Initialized) + return; + + m_Initialized = true; + // build addressables file commands + m_AddressablesBuild.CreateCommand(m_Database); + // Build Bundle Tables + m_AddressablesBuildBundle.CreateCommand(m_Database); + m_AddressablesBuildBundleDependency.CreateCommand(m_Database); + m_AddressablesBuildBundleExpandedDependency.CreateCommand(m_Database); + m_AddressablesBuildBundleRegularDependency.CreateCommand(m_Database); + m_AddressablesBuildBundleDependentBundle.CreateCommand(m_Database); + m_AddressablesBuildBundleFile.CreateCommand(m_Database); + + // Data From Other Asset Tables + m_AddressablesDataFromOtherAsset.CreateCommand(m_Database); + m_AddressablesBuildDataFromOtherAssetObject.CreateCommand(m_Database); + m_AddressablesBuildDataFromOtherAssetObjectReference.CreateCommand(m_Database); + m_AddressablesBuildDataFromOtherAssetReferencingAsset.CreateCommand(m_Database); + + // Explicit Asset Tables + m_AddressablesExplicitAsset.CreateCommand(m_Database); + m_AddressablesBuildExplicitAssetExternallyReferencedAsset.CreateCommand(m_Database); + m_AddressablesBuildExplicitAssetInternalReferencedExplicitAsset.CreateCommand(m_Database); + m_AddressablesBuildExplicitAssetInternalReferencedOtherAsset.CreateCommand(m_Database); + m_AddressablesBuildExplicitAssetLabel.CreateCommand(m_Database); + + // File Tables + m_AddressablesBuildFile.CreateCommand(m_Database); + m_AddressablesBuildFileAsset.CreateCommand(m_Database); + m_AddressablesBuildFileExternalReference.CreateCommand(m_Database); + m_AddressablesBuildFileOtherAsset.CreateCommand(m_Database); + m_AddressablesBuildFileSubFile.CreateCommand(m_Database); + + // Group Tables + m_AddressablesBuildGroup.CreateCommand(m_Database); + m_AddressablesBuildGroupBundle.CreateCommand(m_Database); + m_AddressablesBuildGroupSchema.CreateCommand(m_Database); + + // Schema Tables + m_AddressablesBuildSchema.CreateCommand(m_Database); + m_AddressablesBuildSchemaDataPair.CreateCommand(m_Database); + + // SubFile Tables + m_AddressablesBuildSubFile.CreateCommand(m_Database); + + m_LastId = m_Database.CreateCommand(); + m_LastId.CommandText = "SELECT last_insert_rowid()"; + } + + public void Dispose() + { + // build addressables file commands + m_AddressablesBuild.Dispose(); + // Build Bundle Tables + m_AddressablesBuildBundle.Dispose(); + m_AddressablesBuildBundleDependency.Dispose(); + m_AddressablesBuildBundleExpandedDependency.Dispose(); + m_AddressablesBuildBundleRegularDependency.Dispose(); + m_AddressablesBuildBundleDependentBundle.Dispose(); + m_AddressablesBuildBundleFile.Dispose(); + + // Data From Other Asset Tables + m_AddressablesDataFromOtherAsset.Dispose(); + m_AddressablesBuildDataFromOtherAssetObject.Dispose(); + m_AddressablesBuildDataFromOtherAssetObjectReference.Dispose(); + m_AddressablesBuildDataFromOtherAssetReferencingAsset.Dispose(); + + // Explicit Asset Tables + m_AddressablesExplicitAsset.Dispose(); + m_AddressablesBuildExplicitAssetExternallyReferencedAsset.Dispose(); + m_AddressablesBuildExplicitAssetInternalReferencedExplicitAsset.Dispose(); + m_AddressablesBuildExplicitAssetInternalReferencedOtherAsset.Dispose(); + m_AddressablesBuildExplicitAssetLabel.Dispose(); + + // File Tables + m_AddressablesBuildFile.Dispose(); + m_AddressablesBuildFileAsset.Dispose(); + m_AddressablesBuildFileExternalReference.Dispose(); + m_AddressablesBuildFileOtherAsset.Dispose(); + m_AddressablesBuildFileSubFile.Dispose(); + + // Group Tables + m_AddressablesBuildGroup.Dispose(); + m_AddressablesBuildGroupBundle.Dispose(); + m_AddressablesBuildGroupSchema.Dispose(); + + // Schema Tables + m_AddressablesBuildSchema.Dispose(); + m_AddressablesBuildSchemaDataPair.Dispose(); + + // SubFile Tables + m_AddressablesBuildSubFile.Dispose(); + + m_LastId.Dispose(); + } + + public void WriteAddressablesBuild(string filename, BuildLayout buildLayout) + { + using var transaction = m_Database.BeginTransaction(); + + try + { + m_AddressablesBuild.SetTransaction(transaction); + m_AddressablesBuild.SetValue("name", Path.GetFileName(filename)); + m_AddressablesBuild.SetValue("build_target", buildLayout.BuildTarget); + m_AddressablesBuild.SetValue("start_time", buildLayout.BuildStartTime); + m_AddressablesBuild.SetValue("duration", buildLayout.Duration); + m_AddressablesBuild.SetValue("error", buildLayout.BuildError); + m_AddressablesBuild.SetValue("package_version", buildLayout.PackageVersion); + m_AddressablesBuild.SetValue("player_version", buildLayout.PlayerBuildVersion); + m_AddressablesBuild.SetValue("build_script", buildLayout.BuildScript); + m_AddressablesBuild.SetValue("result_hash", buildLayout.BuildResultHash); + m_AddressablesBuild.SetValue("type", buildLayout.BuildType); + m_AddressablesBuild.SetValue("unity_version", buildLayout.UnityVersion); + m_AddressablesBuild.ExecuteNonQuery(); + + m_LastId.Transaction = transaction; + long buildId = (long)m_LastId.ExecuteScalar(); + if (Verbose) + Console.WriteLine($"Assigned report build ID: {buildId}"); + + foreach (var reference in buildLayout.references.RefIds) + { + switch (reference.type.Class) + { + case "BuildLayout/Bundle": + WriteBuildLayoutBundle(reference, buildId, transaction); + break; + + case "BuildLayout/DataFromOtherAsset": + WriteBuildLayoutDataFromOtherAsset(reference, buildId, transaction); + break; + + case "BuildLayout/ExplicitAsset": + WriteBuildLayoutExplicitAsset(reference, buildId, transaction); + break; + + case "BuildLayout/File": + WriteBuildLayoutFile(reference, buildId, transaction); + break; + + case "BuildLayout/Group": + WriteBuildLayoutGroup(reference, buildId, transaction); + break; + + case "BuildLayout/SchemaData": + WriteBuildLayoutSchemaData(reference, buildId, transaction); + break; + + case "BuildLayout/SubFile": + WriteBuildLayoutSubFile(reference, buildId, transaction); + break; + } + } + + // do the stuff + transaction.Commit(); + } + catch (Exception e) + { + Console.WriteLine(e.StackTrace); + transaction.Rollback(); + throw; + } + } + + private void WriteBuildLayoutBundle(Reference reference, long buildId, SqliteTransaction transaction) + { + m_AddressablesBuildBundle.SetTransaction(transaction); + m_AddressablesBuildBundle.SetValue("id", reference.rid); + m_AddressablesBuildBundle.SetValue("build_id", buildId); + m_AddressablesBuildBundle.SetValue("asset_count", reference.data.AssetCount); + m_AddressablesBuildBundle.SetValue("build_status", reference.data.BuildStatus); + m_AddressablesBuildBundle.SetValue("crc", reference.data.CRC); + m_AddressablesBuildBundle.SetValue("compression", reference.data.Compression); + m_AddressablesBuildBundle.SetValue("dependency_file_size", reference.data.DependencyFileSize); + m_AddressablesBuildBundle.SetValue("expanded_dependency_file_size", reference.data.ExpandedDependencyFileSize); + m_AddressablesBuildBundle.SetValue("file_size", reference.data.FileSize); + m_AddressablesBuildBundle.SetValue("group_rid", reference.data.Group.rid); + m_AddressablesBuildBundle.SetValue("hash", JsonConvert.SerializeObject(reference.data.Hash)); + m_AddressablesBuildBundle.SetValue("internal_name", reference.data.InternalName); + m_AddressablesBuildBundle.SetValue("load_path", reference.data.LoadPath); + m_AddressablesBuildBundle.SetValue("name", reference.data.Name); + m_AddressablesBuildBundle.SetValue("provider", reference.data.Provider); + m_AddressablesBuildBundle.SetValue("result_type", reference.data.ResultType); + m_AddressablesBuildBundle.ExecuteNonQuery(); + + // Insert bundle dependencies + if (reference.data.BundleDependencies != null) + { + foreach (var dep in reference.data.BundleDependencies) + { + m_AddressablesBuildBundleDependency.SetTransaction(transaction); + m_AddressablesBuildBundleDependency.SetValue("bundle_id", reference.rid); + m_AddressablesBuildBundleDependency.SetValue("build_id", buildId); + m_AddressablesBuildBundleDependency.SetValue("dependency_rid", dep.rid); + m_AddressablesBuildBundleDependency.ExecuteNonQuery(); + } + } + + // Insert regular dependencies + if (reference.data.Dependencies != null) + { + foreach (var dep in reference.data.Dependencies) + { + m_AddressablesBuildBundleRegularDependency.SetTransaction(transaction); + m_AddressablesBuildBundleRegularDependency.SetValue("bundle_id", reference.rid); + m_AddressablesBuildBundleRegularDependency.SetValue("build_id", buildId); + m_AddressablesBuildBundleRegularDependency.SetValue("dependency_rid", dep.rid); + m_AddressablesBuildBundleRegularDependency.ExecuteNonQuery(); + } + } + + // Insert dependent bundles + if (reference.data.DependentBundles != null) + { + foreach (var depBundle in reference.data.DependentBundles) + { + m_AddressablesBuildBundleDependentBundle.SetTransaction(transaction); + m_AddressablesBuildBundleDependentBundle.SetValue("bundle_id", reference.rid); + m_AddressablesBuildBundleDependentBundle.SetValue("build_id", buildId); + m_AddressablesBuildBundleDependentBundle.SetValue("dependent_bundle_rid", depBundle.rid); + m_AddressablesBuildBundleDependentBundle.ExecuteNonQuery(); + } + } + + // Insert expanded dependencies + if (reference.data.ExpandedDependencies != null) + { + foreach (var dep in reference.data.ExpandedDependencies) + { + m_AddressablesBuildBundleExpandedDependency.SetTransaction(transaction); + m_AddressablesBuildBundleExpandedDependency.SetValue("bundle_id", reference.rid); + m_AddressablesBuildBundleExpandedDependency.SetValue("build_id", buildId); + m_AddressablesBuildBundleExpandedDependency.SetValue("dependency_rid", dep.rid); + m_AddressablesBuildBundleExpandedDependency.ExecuteNonQuery(); + } + } + + // Insert files + if (reference.data.Files != null) + { + foreach (var file in reference.data.Files) + { + m_AddressablesBuildBundleFile.SetTransaction(transaction); + m_AddressablesBuildBundleFile.SetValue("bundle_id", reference.rid); + m_AddressablesBuildBundleFile.SetValue("build_id", buildId); + m_AddressablesBuildBundleFile.SetValue("file_rid", file.rid); + m_AddressablesBuildBundleFile.ExecuteNonQuery(); + } + } + } + + private void WriteBuildLayoutDataFromOtherAsset(Reference reference, long buildId, SqliteTransaction transaction) + { + m_AddressablesDataFromOtherAsset.SetTransaction(transaction); + m_AddressablesDataFromOtherAsset.SetValue("id", reference.rid); + m_AddressablesDataFromOtherAsset.SetValue("build_id", buildId); + m_AddressablesDataFromOtherAsset.SetValue("asset_guid", reference.data.AssetGuid); + m_AddressablesDataFromOtherAsset.SetValue("asset_path", reference.data.AssetPath); + m_AddressablesDataFromOtherAsset.SetValue("file", reference.data.File.rid); + m_AddressablesDataFromOtherAsset.SetValue("main_asset_type", reference.data.MainAssetType); + m_AddressablesDataFromOtherAsset.SetValue("object_count", reference.data.ObjectCount); + m_AddressablesDataFromOtherAsset.SetValue("serialized_size", reference.data.SerializedSize); + m_AddressablesDataFromOtherAsset.SetValue("streamed_size", reference.data.StreamedSize); + m_AddressablesDataFromOtherAsset.ExecuteNonQuery(); + + // Insert objects + if (reference.data.Objects != null) + { + foreach (var obj in reference.data.Objects) + { + m_AddressablesBuildDataFromOtherAssetObject.SetTransaction(transaction); + m_AddressablesBuildDataFromOtherAssetObject.SetValue("data_from_other_asset_id", reference.rid); + m_AddressablesBuildDataFromOtherAssetObject.SetValue("build_id", buildId); + m_AddressablesBuildDataFromOtherAssetObject.SetValue("asset_type", obj.AssetType); + m_AddressablesBuildDataFromOtherAssetObject.SetValue("component_name", obj.ComponentName ?? ""); + m_AddressablesBuildDataFromOtherAssetObject.SetValue("local_identifier_in_file", obj.LocalIdentifierInFile); + m_AddressablesBuildDataFromOtherAssetObject.SetValue("object_name", obj.ObjectName ?? ""); + m_AddressablesBuildDataFromOtherAssetObject.SetValue("serialized_size", obj.SerializedSize); + m_AddressablesBuildDataFromOtherAssetObject.SetValue("streamed_size", obj.StreamedSize); + m_AddressablesBuildDataFromOtherAssetObject.ExecuteNonQuery(); + + // Insert object references + if (obj.References != null) + { + foreach (var objRef in obj.References) + { + m_AddressablesBuildDataFromOtherAssetObjectReference.SetTransaction(transaction); + m_AddressablesBuildDataFromOtherAssetObjectReference.SetValue("data_from_other_asset_id", reference.rid); + m_AddressablesBuildDataFromOtherAssetObjectReference.SetValue("build_id", buildId); + m_AddressablesBuildDataFromOtherAssetObjectReference.SetValue("local_identifier_in_file", obj.LocalIdentifierInFile); + m_AddressablesBuildDataFromOtherAssetObjectReference.SetValue("asset_id", objRef.AssetId); + m_AddressablesBuildDataFromOtherAssetObjectReference.SetValue("object_id", objRef.ObjectId); + m_AddressablesBuildDataFromOtherAssetObjectReference.ExecuteNonQuery(); + } + } + } + } + + // Insert referencing assets + if (reference.data.ReferencingAssets != null) + { + foreach (var refAsset in reference.data.ReferencingAssets) + { + m_AddressablesBuildDataFromOtherAssetReferencingAsset.SetTransaction(transaction); + m_AddressablesBuildDataFromOtherAssetReferencingAsset.SetValue("data_from_other_asset_id", reference.rid); + m_AddressablesBuildDataFromOtherAssetReferencingAsset.SetValue("build_id", buildId); + m_AddressablesBuildDataFromOtherAssetReferencingAsset.SetValue("referencing_asset_rid", refAsset.rid); + m_AddressablesBuildDataFromOtherAssetReferencingAsset.ExecuteNonQuery(); + } + } + } + + private void WriteBuildLayoutExplicitAsset(Reference reference, long buildId, SqliteTransaction transaction) + { + m_AddressablesExplicitAsset.SetTransaction(transaction); + m_AddressablesExplicitAsset.SetValue("id", reference.rid); + m_AddressablesExplicitAsset.SetValue("build_id", buildId); + m_AddressablesExplicitAsset.SetValue("bundle", reference.data.Bundle.rid); + m_AddressablesExplicitAsset.SetValue("file", reference.data.File.rid); + m_AddressablesExplicitAsset.SetValue("asset_hash", reference.data.AssetHash.Hash); + m_AddressablesExplicitAsset.SetValue("asset_path", reference.data.AssetPath); + m_AddressablesExplicitAsset.SetValue("addressable_name", reference.data.AddressableName); + m_AddressablesExplicitAsset.SetValue("group_guid", reference.data.GroupGuid); + m_AddressablesExplicitAsset.SetValue("guid", reference.data.Guid); + m_AddressablesExplicitAsset.SetValue("internal_id", reference.data.InternalId); + m_AddressablesExplicitAsset.SetValue("streamed_size", reference.data.StreamedSize); + m_AddressablesExplicitAsset.SetValue("serialized_size", reference.data.SerializedSize); + m_AddressablesExplicitAsset.SetValue("main_asset_type", reference.data.MainAssetType); + m_AddressablesExplicitAsset.ExecuteNonQuery(); + + // Insert externally referenced assets + if (reference.data.ExternallyReferencedAssets != null) + { + foreach (var extRefAsset in reference.data.ExternallyReferencedAssets) + { + m_AddressablesBuildExplicitAssetExternallyReferencedAsset.SetTransaction(transaction); + m_AddressablesBuildExplicitAssetExternallyReferencedAsset.SetValue("explicit_asset_id", reference.rid); + m_AddressablesBuildExplicitAssetExternallyReferencedAsset.SetValue("build_id", buildId); + m_AddressablesBuildExplicitAssetExternallyReferencedAsset.SetValue("externally_referenced_asset_rid", extRefAsset.rid); + m_AddressablesBuildExplicitAssetExternallyReferencedAsset.ExecuteNonQuery(); + } + } + + // Insert internal referenced explicit assets + if (reference.data.InternalReferencedExplicitAssets != null) + { + foreach (var intRefExplicitAsset in reference.data.InternalReferencedExplicitAssets) + { + m_AddressablesBuildExplicitAssetInternalReferencedExplicitAsset.SetTransaction(transaction); + m_AddressablesBuildExplicitAssetInternalReferencedExplicitAsset.SetValue("explicit_asset_id", reference.rid); + m_AddressablesBuildExplicitAssetInternalReferencedExplicitAsset.SetValue("build_id", buildId); + m_AddressablesBuildExplicitAssetInternalReferencedExplicitAsset.SetValue("internal_referenced_explicit_asset_rid", intRefExplicitAsset.rid); + m_AddressablesBuildExplicitAssetInternalReferencedExplicitAsset.ExecuteNonQuery(); + } + } + + // Insert internal referenced other assets + if (reference.data.InternalReferencedOtherAssets != null) + { + foreach (var intRefOtherAsset in reference.data.InternalReferencedOtherAssets) + { + m_AddressablesBuildExplicitAssetInternalReferencedOtherAsset.SetTransaction(transaction); + m_AddressablesBuildExplicitAssetInternalReferencedOtherAsset.SetValue("explicit_asset_id", reference.rid); + m_AddressablesBuildExplicitAssetInternalReferencedOtherAsset.SetValue("build_id", buildId); + m_AddressablesBuildExplicitAssetInternalReferencedOtherAsset.SetValue("internal_referenced_other_asset_rid", intRefOtherAsset.rid); + m_AddressablesBuildExplicitAssetInternalReferencedOtherAsset.ExecuteNonQuery(); + } + } + + // Insert labels + if (reference.data.Labels != null) + { + foreach (var label in reference.data.Labels) + { + m_AddressablesBuildExplicitAssetLabel.SetTransaction(transaction); + m_AddressablesBuildExplicitAssetLabel.SetValue("explicit_asset_id", reference.rid); + m_AddressablesBuildExplicitAssetLabel.SetValue("build_id", buildId); + m_AddressablesBuildExplicitAssetLabel.SetValue("label", label); + m_AddressablesBuildExplicitAssetLabel.ExecuteNonQuery(); + } + } + } + + private void WriteBuildLayoutFile(Reference reference, long buildId, SqliteTransaction transaction) + { + m_AddressablesBuildFile.SetTransaction(transaction); + m_AddressablesBuildFile.SetValue("id", reference.rid); + m_AddressablesBuildFile.SetValue("build_id", buildId); + m_AddressablesBuildFile.SetValue("bundle", reference.data.Bundle.rid); + m_AddressablesBuildFile.SetValue("bundle_object_info_size", reference.data.BundleObjectInfo.Size); + m_AddressablesBuildFile.SetValue("mono_script_count", reference.data.MonoScriptCount); + m_AddressablesBuildFile.SetValue("mono_script_size", reference.data.MonoScriptSize); + m_AddressablesBuildFile.SetValue("name", reference.data.Name); + m_AddressablesBuildFile.SetValue("preload_info_size", reference.data.PreloadInfoSize); + m_AddressablesBuildFile.SetValue("write_result_filename", reference.data.WriteResultFilename); + m_AddressablesBuildFile.ExecuteNonQuery(); + + // Insert assets + if (reference.data.Assets != null) + { + foreach (var asset in reference.data.Assets) + { + m_AddressablesBuildFileAsset.SetTransaction(transaction); + m_AddressablesBuildFileAsset.SetValue("file_id", reference.rid); + m_AddressablesBuildFileAsset.SetValue("build_id", buildId); + m_AddressablesBuildFileAsset.SetValue("asset_rid", asset.rid); + m_AddressablesBuildFileAsset.ExecuteNonQuery(); + } + } + + // Insert external references + if (reference.data.ExternalReferences != null) + { + foreach (var extRef in reference.data.ExternalReferences) + { + m_AddressablesBuildFileExternalReference.SetTransaction(transaction); + m_AddressablesBuildFileExternalReference.SetValue("file_id", reference.rid); + m_AddressablesBuildFileExternalReference.SetValue("build_id", buildId); + m_AddressablesBuildFileExternalReference.SetValue("external_reference_rid", extRef.rid); + m_AddressablesBuildFileExternalReference.ExecuteNonQuery(); + } + } + + // Insert other assets + if (reference.data.OtherAssets != null) + { + foreach (var otherAsset in reference.data.OtherAssets) + { + m_AddressablesBuildFileOtherAsset.SetTransaction(transaction); + m_AddressablesBuildFileOtherAsset.SetValue("file_id", reference.rid); + m_AddressablesBuildFileOtherAsset.SetValue("build_id", buildId); + m_AddressablesBuildFileOtherAsset.SetValue("other_asset_rid", otherAsset.rid); + m_AddressablesBuildFileOtherAsset.ExecuteNonQuery(); + } + } + + // Insert sub files + if (reference.data.SubFiles != null) + { + foreach (var subFile in reference.data.SubFiles) + { + m_AddressablesBuildFileSubFile.SetTransaction(transaction); + m_AddressablesBuildFileSubFile.SetValue("file_id", reference.rid); + m_AddressablesBuildFileSubFile.SetValue("build_id", buildId); + m_AddressablesBuildFileSubFile.SetValue("sub_file_rid", subFile.rid); + m_AddressablesBuildFileSubFile.ExecuteNonQuery(); + } + } + } + + private void WriteBuildLayoutGroup(Reference reference, long buildId, SqliteTransaction transaction) + { + m_AddressablesBuildGroup.SetTransaction(transaction); + m_AddressablesBuildGroup.SetValue("id", reference.rid); + m_AddressablesBuildGroup.SetValue("build_id", buildId); + m_AddressablesBuildGroup.SetValue("guid", reference.data.Guid); + m_AddressablesBuildGroup.SetValue("name", reference.data.Name); + m_AddressablesBuildGroup.SetValue("packing_mode", reference.data.PackingMode); + m_AddressablesBuildGroup.ExecuteNonQuery(); + + // Insert bundles + if (reference.data.Bundles != null) + { + foreach (var bundle in reference.data.Bundles) + { + m_AddressablesBuildGroupBundle.SetTransaction(transaction); + m_AddressablesBuildGroupBundle.SetValue("group_id", reference.rid); + m_AddressablesBuildGroupBundle.SetValue("build_id", buildId); + m_AddressablesBuildGroupBundle.SetValue("bundle_rid", bundle.rid); + m_AddressablesBuildGroupBundle.ExecuteNonQuery(); + } + } + + // Insert schemas + if (reference.data.Schemas != null) + { + foreach (var schema in reference.data.Schemas) + { + m_AddressablesBuildGroupSchema.SetTransaction(transaction); + m_AddressablesBuildGroupSchema.SetValue("group_id", reference.rid); + m_AddressablesBuildGroupSchema.SetValue("build_id", buildId); + m_AddressablesBuildGroupSchema.SetValue("schema_rid", schema.rid); + m_AddressablesBuildGroupSchema.ExecuteNonQuery(); + } + } + } + + private void WriteBuildLayoutSchemaData(Reference reference, long buildId, SqliteTransaction transaction) + { + m_AddressablesBuildSchema.SetTransaction(transaction); + m_AddressablesBuildSchema.SetValue("id", reference.rid); + m_AddressablesBuildSchema.SetValue("build_id", buildId); + m_AddressablesBuildSchema.SetValue("guid", reference.data.Guid); + m_AddressablesBuildSchema.SetValue("type", reference.data.Type); + m_AddressablesBuildSchema.ExecuteNonQuery(); + + // Insert schema data pairs + if (reference.data.SchemaDataPairs != null) + { + foreach (var dataPair in reference.data.SchemaDataPairs) + { + m_AddressablesBuildSchemaDataPair.SetTransaction(transaction); + m_AddressablesBuildSchemaDataPair.SetValue("schema_id", reference.rid); + m_AddressablesBuildSchemaDataPair.SetValue("build_id", buildId); + m_AddressablesBuildSchemaDataPair.SetValue("key", dataPair.Key); + m_AddressablesBuildSchemaDataPair.SetValue("value", dataPair.Value); + m_AddressablesBuildSchemaDataPair.ExecuteNonQuery(); + } + } + } + + private void WriteBuildLayoutSubFile(Reference reference, long buildId, SqliteTransaction transaction) + { + m_AddressablesBuildSubFile.SetTransaction(transaction); + m_AddressablesBuildSubFile.SetValue("id", reference.rid); + m_AddressablesBuildSubFile.SetValue("build_id", buildId); + m_AddressablesBuildSubFile.SetValue("is_serialized_file", reference.data.IsSerializedFile ? 1 : 0); + m_AddressablesBuildSubFile.SetValue("name", reference.data.Name); + m_AddressablesBuildSubFile.SetValue("size", reference.data.Size); + m_AddressablesBuildSubFile.ExecuteNonQuery(); + } + } +} diff --git a/Analyzer/SQLite/Writers/SQLiteWriter.cs b/Analyzer/SQLite/Writers/SQLiteWriter.cs new file mode 100644 index 0000000..e3b9d99 --- /dev/null +++ b/Analyzer/SQLite/Writers/SQLiteWriter.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Data.Sqlite; + +namespace UnityDataTools.Analyzer.SQLite.Writers +{ + public class SQLiteWriter : IDisposable + { + private SqliteConnection m_Database; + public SqliteConnection Connection => m_Database; + private string m_DatabaseName; + + + public SQLiteWriter(string databaseName) + { + m_DatabaseName = databaseName; + } + + public void Begin() + { + if (m_Database != null) + { + throw new InvalidOperationException("SQLiteWriter.Begin called twice"); + } + SqliteConnectionStringBuilder builder = new(); + builder.DataSource = m_DatabaseName; + builder.Mode = SqliteOpenMode.ReadWriteCreate; + m_Database = new SqliteConnection(builder.ConnectionString); + File.WriteAllBytes(m_DatabaseName, Array.Empty()); + try + { + m_Database.Open(); + } + catch (Exception e) + { + Console.Error.WriteLine($"Error creating database: {e.Message}"); + } + + // this does all the legacy import of Init.sql + using var command = m_Database.CreateCommand(); + command.CommandText = Resources.Init; + command.ExecuteNonQuery(); + } + + public void End() + { + if (m_Database == null) + { + throw new InvalidOperationException("SQLiteWriter.End called before SQLiteWriter.Begin"); + } + + using var finalizeCommand = m_Database.CreateCommand(); + finalizeCommand.CommandText = Resources.Finalize; + finalizeCommand.ExecuteNonQuery(); + } + + public void Dispose() + { + m_Database?.Dispose(); + m_Database = null; + } + } +} diff --git a/Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs b/Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs new file mode 100644 index 0000000..f91bcd4 --- /dev/null +++ b/Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs @@ -0,0 +1,296 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +using Microsoft.Data.Sqlite; +using UnityDataTools.Analyzer.SQLite.Commands.SerializedFile; +using UnityDataTools.Analyzer.SQLite.Handlers; +using UnityDataTools.Analyzer.Util; +using UnityDataTools.FileSystem; +using UnityDataTools.FileSystem.TypeTreeReaders; + +namespace UnityDataTools.Analyzer.SQLite.Writers; + +public class SerializedFileSQLiteWriter : IDisposable +{ + private HashSet m_TypeSet = new(); + + private int m_CurrentAssetBundleId = -1; + private int m_NextAssetBundleId = 0; + + private bool m_SkipReferences; + + private IdProvider m_SerializedFileIdProvider = new(); + private ObjectIdProvider m_ObjectIdProvider = new(); + + private Regex m_RegexSceneFile = new(@"BuildPlayer-([^\.]+)(?:\.sharedAssets)?"); + + // Used to map PPtr fileId to its corresponding serialized file id in the database. + Dictionary m_LocalToDbFileId = new(); + + private Dictionary m_Handlers = new() + { + { "Mesh", new MeshHandler() }, + { "Texture2D", new Texture2DHandler() }, + { "Shader", new ShaderHandler() }, + { "AudioClip", new AudioClipHandler() }, + { "AnimationClip", new AnimationClipHandler() }, + { "AssetBundle", new AssetBundleHandler() }, + { "PreloadData", new PreloadDataHandler() }, + { "MonoScript", new MonoScriptHandler() }, + { "BuildReport", new BuildReportHandler() }, + { "PackedAssets", new PackedAssetsHandler() }, + }; + + // serialized files + private AddReference m_AddReferenceCommand = new AddReference(); + private AddAssetBundle m_AddAssetBundleCommand = new AddAssetBundle(); + private AddSerializedFile m_AddSerializedFileCommand = new AddSerializedFile(); + private AddObject m_AddObjectCommand = new AddObject(); + private AddType m_AddTypeCommand = new AddType(); + private AddAssetDependency m_InsertDepCommand = new AddAssetDependency(); + + private bool m_Initialized; + private SqliteConnection m_Database; + private SqliteCommand m_LastId = new SqliteCommand(); + private SqliteTransaction m_CurrentTransaction = null; + public SerializedFileSQLiteWriter(SqliteConnection database, bool skipReferences) + { + m_Initialized = false; + m_Database = database; + m_SkipReferences = skipReferences; + } + + public void Init() + { + if (m_Initialized) + return; + + m_Initialized = true; + foreach (var handler in m_Handlers.Values) + { + handler.Init(m_Database); + } + CreateSQLiteCommands(); + } + + private void CreateSQLiteCommands() + { + + // build serialized file commands + m_AddReferenceCommand.CreateCommand(m_Database); + m_AddAssetBundleCommand.CreateCommand(m_Database); + m_AddSerializedFileCommand.CreateCommand(m_Database); + m_AddObjectCommand.CreateCommand(m_Database); + m_AddTypeCommand.CreateCommand(m_Database); + m_InsertDepCommand.CreateCommand(m_Database); + + m_LastId = m_Database.CreateCommand(); + m_LastId.CommandText = "SELECT last_insert_rowid()"; + } + public void BeginAssetBundle(string name, long size) + { + if (m_CurrentAssetBundleId != -1) + { + throw new InvalidOperationException("SQLWriter.BeginAssetBundle called twice"); + } + + m_CurrentAssetBundleId = m_NextAssetBundleId++; + m_AddAssetBundleCommand.SetValue("id", m_CurrentAssetBundleId); + m_AddAssetBundleCommand.SetValue("name", name); + m_AddAssetBundleCommand.SetValue("file_size", size); + m_AddAssetBundleCommand.ExecuteNonQuery(); + } + + public void EndAssetBundle() + { + if (m_CurrentAssetBundleId == -1) + { + throw new InvalidOperationException("SQLWriter.EndAssetBundle called before SQLWriter.BeginAssetBundle"); + } + + m_CurrentAssetBundleId = -1; + } + + public void WriteSerializedFile(string relativePath, string fullPath, string containingFolder) + { + using var sf = UnityFileSystem.OpenSerializedFile(fullPath); + using var reader = new UnityFileReader(fullPath, 64 * 1024 * 1024); + using var pptrReader = new PPtrAndCrcProcessor(sf, reader, containingFolder, AddReference); + int serializedFileId = m_SerializedFileIdProvider.GetId(Path.GetFileName(fullPath).ToLower()); + int sceneId = -1; + + using var transaction = m_Database.BeginTransaction(); + m_CurrentTransaction = transaction; + + var match = m_RegexSceneFile.Match(relativePath); + + if (match.Success) + { + var sceneName = match.Groups[1].Value; + + // There is no Scene object in Unity (a Scene is the full content of a + // SerializedFile). We generate an object id using the name of the Scene + // as SerializedFile name, and the object id 0. + sceneId = m_ObjectIdProvider.GetId((m_SerializedFileIdProvider.GetId(sceneName), 0)); + + // There are 2 SerializedFiles per Scene, one ends with .sharedAssets. This is a + // dirty trick to avoid inserting the scene object a second time. + if (relativePath.EndsWith(".sharedAssets")) + { + m_AddObjectCommand.SetTransaction(transaction); + m_AddObjectCommand.SetValue("game_object", ""); // or other value + m_AddObjectCommand.SetValue("id", sceneId); + m_AddObjectCommand.SetValue("object_id", 0); + m_AddObjectCommand.SetValue("serialized_file", serializedFileId); + // The type is set to -1 which doesn't exist in Unity, but is associated to + // "Scene" in the database. + m_AddObjectCommand.SetValue("type", -1); + m_AddObjectCommand.SetValue("name", sceneName); + m_AddObjectCommand.SetValue("size", 0); + m_AddObjectCommand.SetValue("crc32", 0); + m_AddObjectCommand.ExecuteNonQuery(); + } + } + + m_LocalToDbFileId.Clear(); + + Context ctx = new() + { + AssetBundleId = m_CurrentAssetBundleId, + SerializedFileId = serializedFileId, + SceneId = sceneId, + ObjectIdProvider = m_ObjectIdProvider, + SerializedFileIdProvider = m_SerializedFileIdProvider, + LocalToDbFileId = m_LocalToDbFileId, + Transaction = transaction, + }; + + ctx.Transaction = transaction; + try + { + m_AddSerializedFileCommand.SetTransaction(transaction); + m_AddSerializedFileCommand.SetValue("id", serializedFileId); + m_AddSerializedFileCommand.SetValue("asset_bundle", m_CurrentAssetBundleId == -1 ? "" : m_CurrentAssetBundleId); + m_AddSerializedFileCommand.SetValue("name", relativePath); + m_AddSerializedFileCommand.ExecuteNonQuery(); + + int localId = 0; + m_LocalToDbFileId.Add(localId++, serializedFileId); + foreach (var extRef in sf.ExternalReferences) + { + m_LocalToDbFileId.Add(localId++, + m_SerializedFileIdProvider.GetId(extRef.Path.Substring(extRef.Path.LastIndexOf('/') + 1).ToLower())); + } + + foreach (var obj in sf.Objects) + { + var currentObjectId = m_ObjectIdProvider.GetId((serializedFileId, obj.Id)); + // Console.WriteLine($"\nProcessing {currentObjectId}"); + var root = sf.GetTypeTreeRoot(obj.Id); + var offset = obj.Offset; + uint crc32 = 0; + + if (!m_TypeSet.Contains(obj.TypeId)) + { + m_AddTypeCommand.SetTransaction(transaction); + m_AddTypeCommand.SetValue("id", obj.TypeId); + m_AddTypeCommand.SetValue("name", root.Type); + m_AddTypeCommand.ExecuteNonQuery(); + + m_TypeSet.Add(obj.TypeId); + } + + var randomAccessReader = new RandomAccessReader(sf, root, reader, offset); + + string name = string.Empty; + long streamDataSize = 0; + + if (m_Handlers.TryGetValue(root.Type, out var handler)) + { + handler.Process(ctx, currentObjectId, randomAccessReader, + out name, out streamDataSize); + } + else if (randomAccessReader.HasChild("m_Name")) + { + name = randomAccessReader["m_Name"].GetValue(); + } + + if (randomAccessReader.HasChild("m_GameObject")) + { + var pptr = randomAccessReader["m_GameObject"]; + var fileId = m_LocalToDbFileId[pptr["m_FileID"].GetValue()]; + var gameObjectID = m_ObjectIdProvider.GetId((fileId, pptr["m_PathID"].GetValue())); + m_AddObjectCommand.SetValue("game_object", gameObjectID); + } + else + { + m_AddObjectCommand.SetValue("game_object", ""); + } + + if (!m_SkipReferences) + { + crc32 = pptrReader.Process(currentObjectId, offset, root); + } + + // convert this to the new syntax + m_AddObjectCommand.SetTransaction(transaction); + m_AddObjectCommand.SetValue("id", currentObjectId); + m_AddObjectCommand.SetValue("object_id", obj.Id); + m_AddObjectCommand.SetValue("serialized_file", serializedFileId); + m_AddObjectCommand.SetValue("type", obj.TypeId); + m_AddObjectCommand.SetValue("name", name); + m_AddObjectCommand.SetValue("size", obj.Size + streamDataSize); + m_AddObjectCommand.SetValue("crc32", crc32); + m_AddObjectCommand.ExecuteNonQuery(); + + // If this is a Scene AssetBundle, add the object as a depencency of the + // current scene. + if (ctx.SceneId != -1) + { + m_InsertDepCommand.SetTransaction(transaction); + m_InsertDepCommand.SetValue("object", ctx.SceneId); + m_InsertDepCommand.SetValue("dependency", currentObjectId); + m_InsertDepCommand.ExecuteNonQuery(); + } + } + + transaction.Commit(); + } + catch (Exception) + { + transaction.Rollback(); + throw; + } + } + + private int AddReference(long objectId, int fileId, long pathId, string propertyPath, string propertyType) + { + var referencedObjectId = m_ObjectIdProvider.GetId((m_LocalToDbFileId[fileId], pathId)); + m_AddReferenceCommand.SetTransaction(m_CurrentTransaction); + m_AddReferenceCommand.SetValue("object", objectId); + m_AddReferenceCommand.SetValue("referenced_object", referencedObjectId); + m_AddReferenceCommand.SetValue("property_path", propertyPath); + m_AddReferenceCommand.SetValue("property_type", propertyType); + m_AddReferenceCommand.ExecuteNonQuery(); + return referencedObjectId; + } + + public void Dispose() + { + foreach (var handler in m_Handlers.Values) + { + handler.Dispose(); + } + + // Serialized file dispose calls + m_AddAssetBundleCommand.Dispose(); + m_AddSerializedFileCommand.Dispose(); + m_AddReferenceCommand.Dispose(); + m_AddObjectCommand.Dispose(); + m_AddTypeCommand.Dispose(); + m_InsertDepCommand.Dispose(); + + m_LastId.Dispose(); + } +} diff --git a/Analyzer/SerializedObjects/AnimationClip.cs b/Analyzer/SerializedObjects/AnimationClip.cs index dbbc7ce..4d3fd03 100644 --- a/Analyzer/SerializedObjects/AnimationClip.cs +++ b/Analyzer/SerializedObjects/AnimationClip.cs @@ -7,8 +7,8 @@ public class AnimationClip public string Name { get; init; } public bool Legacy { get; init; } public int Events { get; init; } - - private AnimationClip() {} + + private AnimationClip() { } public static AnimationClip Read(RandomAccessReader reader) { @@ -19,4 +19,4 @@ public static AnimationClip Read(RandomAccessReader reader) Events = reader["m_Events"].GetArraySize() }; } -} \ No newline at end of file +} diff --git a/Analyzer/SerializedObjects/AssetBundle.cs b/Analyzer/SerializedObjects/AssetBundle.cs index 20d20f1..bd8b58d 100644 --- a/Analyzer/SerializedObjects/AssetBundle.cs +++ b/Analyzer/SerializedObjects/AssetBundle.cs @@ -4,7 +4,7 @@ namespace UnityDataTools.Analyzer.SerializedObjects; public class AssetBundle -{ +{ public string Name { get; init; } public IReadOnlyList Assets { get; init; } public IReadOnlyList PreloadTable { get; init; } @@ -17,7 +17,7 @@ public class Asset public int PreloadIndex { get; init; } public int PreloadSize { get; init; } - private Asset() {} + private Asset() { } public static Asset Read(RandomAccessReader reader) { @@ -30,21 +30,21 @@ public static Asset Read(RandomAccessReader reader) }; } } - - private AssetBundle() {} - + + private AssetBundle() { } + public static AssetBundle Read(RandomAccessReader reader) { var name = reader["m_Name"].GetValue(); var assets = new List(reader["m_Container"].GetArraySize()); var preloadTable = new List(reader["m_PreloadTable"].GetArraySize()); var isSceneAssetBundle = reader["m_IsStreamedSceneAssetBundle"].GetValue(); - + foreach (var pptr in reader["m_PreloadTable"]) { preloadTable.Add(PPtr.Read(pptr)); } - + foreach (var asset in reader["m_Container"]) { assets.Add(Asset.Read(asset)); @@ -58,4 +58,4 @@ public static AssetBundle Read(RandomAccessReader reader) IsSceneAssetBundle = isSceneAssetBundle }; } -} \ No newline at end of file +} diff --git a/Analyzer/SerializedObjects/AudioClip.cs b/Analyzer/SerializedObjects/AudioClip.cs index 2aae772..d228b81 100644 --- a/Analyzer/SerializedObjects/AudioClip.cs +++ b/Analyzer/SerializedObjects/AudioClip.cs @@ -12,7 +12,7 @@ public class AudioClip public int LoadType { get; init; } public int Format { get; init; } - private AudioClip() {} + private AudioClip() { } public static AudioClip Read(RandomAccessReader reader) { @@ -27,4 +27,4 @@ public static AudioClip Read(RandomAccessReader reader) StreamDataSize = reader["m_Resource"]["m_Size"].GetValue() }; } -} \ No newline at end of file +} 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/Mesh.cs b/Analyzer/SerializedObjects/Mesh.cs index 8df6a72..4f0076a 100644 --- a/Analyzer/SerializedObjects/Mesh.cs +++ b/Analyzer/SerializedObjects/Mesh.cs @@ -23,7 +23,7 @@ public enum ChannelUsage BlendWeights, BlendIndices, }; - + public enum ChannelType { Float, @@ -56,11 +56,11 @@ public class Channel public int Vertices { get; init; } public int Compression { get; init; } public bool RwEnabled { get; init; } - + public IReadOnlyList Channels { get; init; } - + public int VertexSize { get; init; } - + private static readonly int[] s_ChannelTypeSizes = { 4, // Float @@ -77,7 +77,7 @@ public class Channel 4, // SInt32 }; - private Mesh() {} + private Mesh() { } public static Mesh Read(RandomAccessReader reader) { @@ -95,7 +95,7 @@ public static Mesh Read(RandomAccessReader reader) indices = reader["m_IndexBuffer"].GetArraySize() / bytesPerIndex; vertices = reader["m_VertexData"]["m_VertexCount"].GetValue(); - + // If vertex data size is 0, data is stored in a stream file. if (reader["m_VertexData"]["m_DataSize"].GetArraySize() == 0) { @@ -123,7 +123,7 @@ public static Mesh Read(RandomAccessReader reader) Type = (ChannelType)channel["format"].GetValue(), Usage = (ChannelUsage)i, }; - + channels.Add(c); vertexSize += dimension * s_ChannelTypeSizes[(int)c.Type]; } @@ -136,7 +136,7 @@ public static Mesh Read(RandomAccessReader reader) vertices = reader["m_CompressedMesh"]["m_Vertices"]["m_NumItems"].GetValue() / 3; indices = reader["m_CompressedMesh"]["m_Triangles"]["m_NumItems"].GetValue(); } - + return new Mesh() { Name = name, @@ -151,4 +151,4 @@ public static Mesh Read(RandomAccessReader reader) VertexSize = vertexSize, }; } -} \ No newline at end of file +} 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/PPtr.cs b/Analyzer/SerializedObjects/PPtr.cs index c44054c..f024997 100644 --- a/Analyzer/SerializedObjects/PPtr.cs +++ b/Analyzer/SerializedObjects/PPtr.cs @@ -6,8 +6,8 @@ public class PPtr { public int FileId { get; init; } public long PathId { get; init; } - - private PPtr() {} + + private PPtr() { } public static PPtr Read(RandomAccessReader reader) { 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/SerializedObjects/PreloadData.cs b/Analyzer/SerializedObjects/PreloadData.cs index 429c0cb..f010dc4 100644 --- a/Analyzer/SerializedObjects/PreloadData.cs +++ b/Analyzer/SerializedObjects/PreloadData.cs @@ -6,13 +6,13 @@ namespace UnityDataTools.Analyzer.SerializedObjects; public class PreloadData { public IReadOnlyList Assets { get; init; } - - private PreloadData() {} + + private PreloadData() { } public static PreloadData Read(RandomAccessReader reader) { var assets = new List(reader["m_Assets"].GetArraySize()); - + foreach (var pptr in reader["m_Assets"]) { assets.Add(PPtr.Read(pptr)); diff --git a/Analyzer/SerializedObjects/SerializedObject.cs b/Analyzer/SerializedObjects/SerializedObject.cs deleted file mode 100644 index ba29ff2..0000000 --- a/Analyzer/SerializedObjects/SerializedObject.cs +++ /dev/null @@ -1,9 +0,0 @@ -using UnityDataTools.FileSystem.TypeTreeReaders; - -namespace UnityDataTools.Analyzer.SerializedObjects; - -public abstract class SerializedObject -{ - protected SerializedObject() {} - protected SerializedObject(RandomAccessReader reader) {} -} diff --git a/Analyzer/SerializedObjects/Shader.cs b/Analyzer/SerializedObjects/Shader.cs index 4fb495a..c53a568 100644 --- a/Analyzer/SerializedObjects/Shader.cs +++ b/Analyzer/SerializedObjects/Shader.cs @@ -10,29 +10,29 @@ public class Shader public IReadOnlyList SubShaders { get; init; } public IReadOnlyList Keywords { get; init; } - private Shader() {} + private Shader() { } public class SubShader { public IReadOnlyList Passes { get; init; } - + public class Pass { public string Name { get; init; } // The key is the program type (vertex, fragment...) - public IReadOnlyDictionary> Programs { get; init; } - + public IReadOnlyDictionary> Programs { get; init; } + public class SubProgram { public int HwTier { get; init; } public int Api { get; init; } public uint BlobIndex { get; init; } - + // Keyword index in ShaderData.Keywords public IReadOnlyList Keywords { get; init; } - - private SubProgram() {} + + private SubProgram() { } public static SubProgram Read(KeywordSet keywordSet, RandomAccessReader reader, Dictionary keywordNames, int hwTier = -1) { @@ -71,8 +71,8 @@ public static SubProgram Read(KeywordSet keywordSet, RandomAccessReader reader, } } } - - return new SubProgram() { Api = api, BlobIndex = blobIndex, HwTier = hwTier , Keywords = keywords }; + + return new SubProgram() { Api = api, BlobIndex = blobIndex, HwTier = hwTier, Keywords = keywords }; } } @@ -80,7 +80,7 @@ public static Pass Read(KeywordSet keywordSet, RandomAccessReader reader, Dictio { string name = null; Dictionary> programsPerType = new(); - + if (keywordNames == null) { keywordNames = new(); @@ -144,12 +144,12 @@ public static Pass Read(KeywordSet keywordSet, RandomAccessReader reader, Dictio if (subPrograms.Count > 0) { var programs = new List(subPrograms.GetArraySize()); - + foreach (var subProgram in subPrograms) { programs.Add(SubProgram.Read(keywordSet, subProgram, keywordNames)); } - + programsPerType[progType.typeName] = programs; } } @@ -163,13 +163,13 @@ public static SubShader Read(KeywordSet keywordSet, RandomAccessReader reader, D { var passesReader = reader["m_Passes"]; var passes = new List(passesReader.GetArraySize()); - + foreach (var pass in passesReader) { passes.Add(Pass.Read(keywordSet, pass, keywordNames)); } - return new SubShader() {Passes = passes}; + return new SubShader() { Passes = passes }; } } @@ -192,13 +192,13 @@ public static Shader Read(RandomAccessReader reader) } var subShadersReader = parsedForm["m_SubShaders"]; - List subShaders = new (subShadersReader.GetArraySize()); - + List subShaders = new(subShadersReader.GetArraySize()); + foreach (var subShader in subShadersReader) { subShaders.Add(SubShader.Read(keywordSet, subShader, keywordNames)); } - + int decompressedSize = 0; if (reader["decompressedLengths"].IsArrayOfObjects) @@ -244,11 +244,11 @@ public class KeywordSet private List m_Keywords = new(); private Dictionary m_KeywordToIndex = new(); - + public int GetKeywordIndex(string name) { int index; - + if (m_KeywordToIndex.TryGetValue(name, out index)) { return index; @@ -261,4 +261,4 @@ public int GetKeywordIndex(string name) return index; } } -} \ No newline at end of file +} diff --git a/Analyzer/SerializedObjects/Texture2D.cs b/Analyzer/SerializedObjects/Texture2D.cs index ddd7630..2fa2774 100644 --- a/Analyzer/SerializedObjects/Texture2D.cs +++ b/Analyzer/SerializedObjects/Texture2D.cs @@ -1,5 +1,5 @@ -using UnityDataTools.FileSystem.TypeTreeReaders; using System.Text.Json; +using UnityDataTools.FileSystem.TypeTreeReaders; namespace UnityDataTools.Analyzer.SerializedObjects; public class Texture2D @@ -12,8 +12,8 @@ public class Texture2D public int MipCount { get; init; } public bool RwEnabled { get; init; } - private Texture2D() {} - + private Texture2D() { } + public static Texture2D Read(RandomAccessReader reader) { return new Texture2D() 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/Analyzer/Util/IdProvider.cs b/Analyzer/Util/IdProvider.cs index cd5fefb..82ad30f 100644 --- a/Analyzer/Util/IdProvider.cs +++ b/Analyzer/Util/IdProvider.cs @@ -21,7 +21,7 @@ public IdProvider(bool bidirectional = false, IEqualityComparer comparer = public int GetId(Key key) { int id; - + if (m_Ids.TryGetValue(key, out id)) { return id; @@ -51,5 +51,5 @@ public Key GetKey(int id) public class ObjectIdProvider : IdProvider<(int fileId, long pathId)> { - public ObjectIdProvider(bool bidirectional = false) : base(bidirectional) {} + public ObjectIdProvider(bool bidirectional = false) : base(bidirectional) { } } diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/Documentation/AddressableBuildComparison.png b/Documentation/AddressableBuildComparison.png new file mode 100644 index 0000000..c3f9a10 Binary files /dev/null and b/Documentation/AddressableBuildComparison.png differ diff --git a/Documentation/AddressableSerializedFileComparison.png b/Documentation/AddressableSerializedFileComparison.png new file mode 100644 index 0000000..e56384d Binary files /dev/null and b/Documentation/AddressableSerializedFileComparison.png differ diff --git a/Documentation/AssetBundleBuildComparison.png b/Documentation/AssetBundleBuildComparison.png new file mode 100644 index 0000000..fa2d192 Binary files /dev/null and b/Documentation/AssetBundleBuildComparison.png differ diff --git a/Documentation/AssetBundleContentComparison.png b/Documentation/AssetBundleContentComparison.png new file mode 100644 index 0000000..fbd78ef Binary files /dev/null and b/Documentation/AssetBundleContentComparison.png differ 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/RemoveHashName.png b/Documentation/RemoveHashName.png new file mode 100644 index 0000000..92c9425 Binary files /dev/null and b/Documentation/RemoveHashName.png differ diff --git a/Documentation/ResFileBinaryDiff.png b/Documentation/ResFileBinaryDiff.png new file mode 100644 index 0000000..5c34ff7 Binary files /dev/null and b/Documentation/ResFileBinaryDiff.png differ diff --git a/Documentation/SpritesBundleContent.png b/Documentation/SpritesBundleContent.png new file mode 100644 index 0000000..949773d Binary files /dev/null and b/Documentation/SpritesBundleContent.png differ diff --git a/Documentation/SpritesBundleDetailedContent.png b/Documentation/SpritesBundleDetailedContent.png new file mode 100644 index 0000000..58427ce Binary files /dev/null and b/Documentation/SpritesBundleDetailedContent.png differ diff --git a/Documentation/TypeTreeForPlayer.png b/Documentation/TypeTreeForPlayer.png new file mode 100644 index 0000000..391cdfa Binary files /dev/null and b/Documentation/TypeTreeForPlayer.png differ diff --git a/Documentation/addressables-build-reports.md b/Documentation/addressables-build-reports.md new file mode 100644 index 0000000..de05faf --- /dev/null +++ b/Documentation/addressables-build-reports.md @@ -0,0 +1,181 @@ +# Addressables Build Report Analysis + +## Overview + +Unity Data Tools provides the ability to analyse Unity Addressables build reports. The tool extracts detailed information about bundles, assets, dependencies, file sizes, and build performance metrics. This information can complement extracting information from the Asset Bundles directly. + +When you run the analyzer on a directory containing Addressables build reports, the tool will parse them and add the data to the sqlite database. + +Each build report is assigned a unique id that is used as the id in addressables_builds and build_id field in subsequent tables. This allows you to compare builds against each other. + +## Concepts + +**Builds** +: a build corresponds to a content build. This can be part of a player build, or standalone through the Addressables groups window + +**Bundles** +: asset bundles that are output by the build. + +**Groups** +: groups in the Addressable Groups window whose settings generate one or more Asset Bundles + +**Schemas** +: settings for groups that determine how bundles are generated + +**Files** +: the file in the asset bundle that contains serialized files + +**SubFiles** +: files that are bundled in the asset bundle, but not stored with the rest of the serialized files (resS, scene sharedAssets) + +**Explicit Assets** +: these are assets that have had the Addressable checkbox checked in the Editor + +**Other Assets** +: these are assets that are included because an explicit asset depends upon them + +## Database Schema + +The Addressables build data is stored across multiple related tables in the SQLite database prefixed with addressables_. + + +### Core Tables + +#### `addressables_builds` +Main build information table + * id maps to build_id in other tables + +#### `addressables_build_groups` +Contains groups used in the build and whether they're pack separate or together. + * guid maps to group_guid in other tables + +#### `addressables_build_group_schemas` +Map groups to their schemas + * schema_rid maps to addressables_group_schemas.id + * group_id maps to addressables_build_groups.id + +#### `addressables_build_schemas` +Contain schema names. + * id maps to addressables_group_schemas.id + +#### `add_build_schema_data_pairs` +Contains key value pairs of schema settings at time of build. + * schema_id maps to addressables_build_schemas.id + +#### `addressables_build_bundles` +Bundle-level information including asset counts and file sizes. + +#### `addressables_build_bundle_dependent_bundles` +Maps bundles to the bundles they depend upon (dependent bundles will be loaded as long as the bundle in question is loaded). + * bundle_id maps to addressables_build_bundles.id + * dependent_bundle_rid maps to addressables_build_bundles.id + +#### `addressables_build_bundle_files` +List files in bundles. These are the serialized files and external files. + * bundle_id maps to addressables_build_bundles.id + * file_rid maps to addressables_build_files.id + +#### `addressables_build_explicit_assets` +Explicit assets (marked as Addressable). Has Addressable name and asset information including paths. + * bundle maps to addressables_build_bundles.id + * group_guid maps to addressables_build_groups.guid + * file maps to addressables_build_files.id + +#### `addressables_build_explicit_asset_internal_referenced_other_assets` +Map explicit assets to other assets they refer to. For instance a prefab to its underlying FBX + * referencing_asset_rid maps to addressables_build_explicit_assets.id + * data_from_other_asset_Id maps to addressables_build_data_from_other_assets.id + +#### `addressables_build_data_from_other_assets` +Assets added into the build implicitly by explicitly defined assets. + * file maps to addressables_build_files.id + +#### `addressables_build_cached_bundles` +A view that contains the filename of the built bundle and the name it is stored in the Unity runtime cache. + +### Basic Analysis + +To analyze Addressables build reports in your project: + +```bash +# Analyze all files in a directory (automatically detects Addressables JSON files) +UnityDataTools.exe "Library\\com.unity.addressables\\BuildReports\\" -o "addressables_analysis.db" + +# Include verbose output to see processing details +UnityDataTools.exe "Library\\com.unity.addressables\\BuildReports\\" -o "addressables_analysis.db" --verbose + +# Analyze only JSON files specifically +UnityDataTools.exe "C:\\Temp\\MyExtractedFiles" -o "addressables_analysis.db" -p "*.json" +``` + +You can analyze a directory with both asset bundles (*.bundle) and json files (*.json) at the same time. + +### Sample Queries + +Once the data is in the database, you can run queries to analyze your Addressables build: + + +#### Find all implicit assets for an explicit asset +```sql +-- Find implicitly included assets for a given explicit asset id +SELECT a.explicit_asset_id, b.id, b.asset_path, b.asset_path + FROM addressables_build_explicit_asset_internal_referenced_other_assets a, + addressables_build_data_from_other_assets b + WHERE a.internal_referenced_other_asset_rid = b.id + AND a.build_id = b.build_id; + AND a.explicit_asset_id = 5092 + AND a.build_id = 3; +``` + +#### Find the cache name for an addressables bundle +Addressables renames bundles to make it possible to do content updates. Internally bundles are still named by their internal hash and are cached based upon this name. If you want to lookup how a remote bundle will be cached in Unity's [cache](https://docs.unity3d.com/ScriptReference/Caching.html) you can use the addressables_build_cached_bundles view. +```sql +-- Find cache name for an addressables bundle +SELECT cached_name + FROM addressables_build_cached_bundles + WHERE name_in_catalog = 'sharedenvironment_assets_all_5935f9c20c9b10664721f1591e3d2036.bundle' + AND build_id = 1; +``` + +#### Bundle Size Analysis +This query works for groups that are packed together. It assumes group guids stay the same between builds and compares the bundle file size for a group between builds. +```sql +-- Compare bundle size changes between builds +SELECT second_build.name as bundle_name, second_build.file_size as second_build_file_size, second_build.asset_count as second_build_asset_count, +first_build.file_size as first_build_File_size, first_build.asset_count as first_build_asset_count +FROM addressables_build_bundles first_build, + addressables_build_bundles second_build, + addressables_build_groups g1, + addressables_build_groups g2 + WHERE second_build.file_size > first_build.file_size + AND first_build.group_rid = g1.id + AND second_build.group_rid = g2.id + AND g1.guid = g2.guid + AND first_build.build_id = 1 + AND second_build.build_id = 2 + AND g1.build_id = first_build.build_id + AND g2.build_id = second_build.build_id + AND second_build.file_size != first_build.file_size + ORDER BY second_build.file_size DESC; +``` + +#### Build Performance Analysis +```sql +-- Analyze build duration and asset counts for successful builds +SELECT + a.id, + a.name, + a.duration, + COUNT(b.id) as bundle_count, + SUM(b.asset_count) as total_assets, + SUM(b.file_size) as total_size +FROM addressables_builds a +LEFT JOIN addressables_build_bundles b ON a.id = b.build_id +WHERE coalesce(a.error, '') = '' +group by a.id; + +-- Find builds with errors +SELECT name, start_time, duration, error +FROM addressables_builds +WHERE coalesce(error, '') != ''; +``` diff --git a/Documentation/analyze-examples.md b/Documentation/analyze-examples.md new file mode 100644 index 0000000..23f5551 --- /dev/null +++ b/Documentation/analyze-examples.md @@ -0,0 +1,263 @@ +# Example usage of Analyze + +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.md#analyzeanalyse). + +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 + +You can find data in the SQLite database by running SQL queries. + +Graphical tools such as "DB Browser" offer a way to run these queries directly from the UI based on whatever database you have open. + +However often it is useful to run queries from the command line, and to incorporate queries into your scripts (bash, PowerShell, etc). Some of the example on this page show the command line syntax for running simple queries. + +These examples assume you have `sqlite3` available in the path for your command prompt or terminal. On Windows that means that a directory containing `sqlite3.exe` is included in your PATH environmental variable. + +On Windows, sqlite3.exe is available as part of the "SQLite command line tools", published from [www.sqlite.org](www.sqlite.org). + +Note: Many of the examples in this topic assume that your database file is named `Analysis.db` in your current working directory. + +Disclaimer: The command line examples and scripts will need to be modified for your specific needs, and may need to be rewritten for your platform and preferred command line environment. It is a best practice to only run commands that you fully understand from a command prompt. + +## Example: Object count + +Starting things simple: running the following command on a command prompt will invoke a query will print the total number of objects in the build. + +``` +sqlite3 Analysis.db "SELECT COUNT(*) FROM objects;" +``` + +## Example: Shader information + +shader_view has a lot of useful information about shaders. For example to see the list of keywords for a particular shader, try the following command. This should work with both Powershell and Bash: +``` +sqlite3 Analysis.db ".mode column" "SELECT keywords FROM shader_view WHERE name = 'Sprites/Default';" +``` + +Example output: + +``` +keywords +------------------- +PIXELSNAP_ON, +INSTANCING_ON, +ETC1_EXTERNAL_ALPHA +``` + +Another example query is the top 5 shaders by size. + +``` +sqlite3 Analysis.db ".mode column" "SELECT name, pretty_size, serialized_file FROM shader_view ORDER BY size DESC LIMIT 5;" +``` + +Example output (based on a build of the [megacity-metro](https://github.com/Unity-Technologies/megacity-metro) project): +``` +name pretty_size serialized_file +------------------------------------------ ----------- -------------------------------- +TextMeshPro/Mobile/Distance Field 191.5 KB resources.assets +Hidden/Universal Render Pipeline/UberPost 144.3 KB globalgamemanagers.assets +Shader Graphs/CustomLightingBuildingsB_LOD 139.6 KB 1b2fdfe013c58ffd57d7663eb8db3e60 +Universal Render Pipeline/Lit 115.5 KB 1b2fdfe013c58ffd57d7663eb8db3e60 +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 + +This is not a tutorial on using AI tools. However one useful tip: + +Many AI tools let you provide context by uploading a file or copying text. They are helpful for crafting SQL statements and creating scripts. However by default they probably do not know what to expect inside a UnityDataTools SQLite database. + +To provide this information you could run this command that dumps the current schema into a text file. + +``` +sqlite3 Analysis.db ".schema" > schema_dump.sql.txt +``` + +Then provide that file as context, prior to asking it to write queries based on the available tables, views and columns. For example: *Help me write a command line calling sqlite3 for Analysis.db that will print the top 5 shaders by the size column. It will print the name, pretty_size and serialized_file.* + +## Example: Finding AssetBundles containing a certain object type + +If you want to find out which AssetBundles in a build contain a certain object type you can try a query like this: + +``` +sqlite3 Analysis.db "SELECT DISTINCT asset_bundle FROM object_view WHERE type = 'MonoBehaviour';" +``` + +The above query takes advantage of the object_view which pulls together the data from multiple tables. The following query does exactly the same thing, but uses the underlying tables directly: + +``` +sqlite3 Analysis.db "SELECT DISTINCT ab.name AS asset_bundle FROM objects o INNER JOIN types t ON o.type = t.id INNER JOIN serialized_files sf ON o.serialized_file = sf.id LEFT JOIN asset_bundles ab ON sf.asset_bundle = ab.id WHERE t.name = 'MonoBehaviour';" +``` + +Note: Both MonoBehaviours and ScriptableObjects have the same serialized type "MonoBehaviour". + + +## Example: Finding instances of a scripting class + +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 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) SpriteSkin + m_ExecutionOrder (int) 0 + m_PropertiesHash (Hash128) + ... + m_ClassName (string) SpriteSkin + m_Namespace (string) UnityEngine.U2D.Animation + m_AssemblyName (string) Unity.2D.Animation.Runtime +``` + +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 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. + +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 monoscript_view ms ON r.referenced_object = ms.id +WHERE mb.type = 'MonoBehaviour' + AND r.property_type = 'MonoScript' + AND ms.class_name = 'SpriteSkin' + AND ms.namespace = 'UnityEngine.U2D.Animation'; +``` + +## Example: Quick summary for individual AssetBundles + +Often Analyze is used for an entire build output, so that you can view information about the build output as a whole. +However it can also be used in a more light weight fashion for quickly printing information about a specific AssetBundle. +Typically the time to run analyze on a single file should be very fast, so it can be acceptable to use a temporary database and +erase it immediately after that. + +This is an example if you want to look at an AssetBundle called sprites.bundle in the current working directory. + +``` +UnityDataTool analyze . -o sprites.bundle.db -p sprites.bundle +sqlite3 .\sprites.bundle.db ".mode column" "SELECT object_id, type, name, pretty_size, crc32 from object_view" +``` + +After running this sprites.bundle.db is not needed anymore, and could be erased (e.g. using a platform/shell appropriate command like "del" or "rm"). + +The following is an example PowerShell script that generalizes the idea: + +``` +# PowerShell Script to run UnityDataTool on a single AssetBundle (or single SerializedFile) and print out a summary of the objects. + +param( + [Parameter(Mandatory=$true)] + [string]$FileName +) + +if (-not (Test-Path -Path $FileName)) { + Write-Error "File '$FileName' does not exist." + exit 1 +} + +#Query to run on the temporary analyze database +$select_statement = "SELECT object_id, type, name, pretty_size, crc32 from object_view" + +# Separate the directory and file name +$FileDir = Split-Path -Path $FileName -Parent +$FileLeaf = Split-Path -Path $FileName -Leaf + +# If no directory is detected (relative file name), use the current working directory +if (-not $FileDir) { + $FileDir = "." +} + +# Retrieve the system's temp folder and create the temporary database file name +$tempFolder = $env:TEMP +$dbName = Join-Path -Path $tempFolder -ChildPath ("$FileLeaf.db") + +try { + # Run UnityDataTool + UnityDataTool analyze $FileDir -o $dbName -p $FileLeaf + + # Query the database using sqlite3 + sqlite3 $dbName ".mode column" $select_statement + + # Delete the temporary database file + Remove-Item $dbName -Force +} catch { + Write-Error "An error occurred: $_" +} +``` + +Here is an example output from the script above (where the script has been saved as `objectlist.ps1` somewhere in the Path): + +``` +>objectlist.ps1 .\AssetBundles\sprites.bundle + +... + +object_id type name pretty_size crc32 +-------------------- ----------- -------------- ----------- ---------- +-3600607445234681765 Texture2D red 148.5 KB 3115177070 +-2408881041259534328 Sprite Snow 460.0 B 2324949527 +-1350043613627603771 Texture2D Snow 512.2 KB 3894005184 +1 AssetBundle sprites.bundle 332.0 B 2353941696 +3866367853307903194 Sprite red 460.0 B 1811343945 +``` + +## Example: Finding differences between two builds + +This is a large subject, see [Comparing Builds](comparing-builds.md). + +## Example: Matching content back to the source asset + +UnityDataTool works on the output of a Unity build, which, by its very nature, only contains the crucial data needed to efficiently load built content in the Player. It does not include complete information about the assets and scenes in the project that was used to create that build. You may want to match content back to the original source asset or scene. For example if the size of an AssetBundle has unexpectedly changed between builds then you may want to track down which source assets could be responsible for that change. Or you may want to confirm that some particular image has been included in the build. + +For AssetBundles partial asset information can be found in the m_Containers list, inside the AssetBundle object. This records assets that were explicitly added to AssetBundles. In the database this can be found in the `assets` table. However, assets that are included in the build implicitly (because they are referenced from the explicitly added assets) will not be recorded anywhere in the AssetBundle content. + +Similarly for a player build the only paths populated in the `assets` table are the scenes from the Build Profile Scene List. The paths of the assets in the sharedAsset files is not recorded anywhere in the build output. + +In many cases the source asset can be inferred based on your specific knowledge of your project, and how the build was configured. For example the level files in a Player build match the Scenes in the Build Profile Scene list. And the content of AssetBundles is driven from the assignment of specific assets to those AssetBundles (or Addressable groups), along with assets they depend on. + +Also, in many cases the name of objects matches the file name of the asset. For example the Texture2D "red" object probably comes from a file named red.png somewhere in the project. + +Similarly, it may be possible to find an object based on a distinctive property values, such as a string or hash, by doing text-based searches in the output from the `dump` command. + +For more precise information about how Source Assets contribute to the build result it may be better to consult files that are produced during the build process, instead of UnityDataTools. + +Examples of alternative sources of build information: + +* The [BuildReport](https://docs.unity3d.com/ScriptReference/Build.Reporting.BuildReport.html) has detailed source information in the PackedAssets section. The [BuildReportInspector](https://github.com/Unity-Technologies/BuildReportInspector) is a useful way to view data from the BuildReport. +* The Editor log reports a lot of information during a build. +* Regular AssetBundle builds create [.manifest files](https://docs.unity3d.com/Manual/assetbundles-file-format.html), which contain information about the source assets and types. +* Addressable builds do not produce BuildReport files, nor .manifest files. But UnityDataTools supports analyzing the [Addressables Build Reports](addressables-build-reports.md) and will populate the `addressables_build_explicit_assets` and `addressables_build_data_from_other_assets` tables. 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/Documentation/command-analyze.md b/Documentation/command-analyze.md new file mode 100644 index 0000000..16c4553 --- /dev/null +++ b/Documentation/command-analyze.md @@ -0,0 +1,136 @@ +# analyze Command + +The `analyze` command extracts information from Unity Archives (e.g. AssetBundles) and SerializedFiles and dumps the results into a SQLite database. + +## Quick Reference + +``` +UnityDataTool analyze [options] +``` + +| Option | Description | Default | +|--------|-------------|---------| +| `` | Path to folder containing files to analyze | *(required)* | +| `-o, --output-file ` | Output database filename | `database.db` | +| `-p, --search-pattern ` | File search pattern (`*` and `?` supported) | `*` | +| `-s, --skip-references` | Skip CRC and reference extraction (faster, smaller DB) | `false` | +| `-v, --verbose` | Show more information during analysis | `false` | +| `--no-recurse` | Do not recurse into sub-directories | `false` | + +## Examples + +Analyze all files in a directory: +```bash +UnityDataTool analyze /path/to/asset/bundles +``` + +Analyze only `.bundle` files and specify a custom database name: +```bash +UnityDataTool analyze /path/to/asset/bundles -o my_database.db -p "*.bundle" +``` + +Fast analysis (skip reference tracking): +```bash +UnityDataTool analyze /path/to/bundles -s +``` + +See also [Analyze Examples](../../Documentation/analyze-examples.md). + +--- + +## What Can Be Analyzed + +The analyze command works with the following types of directories: + +| Input Type | Description | +|------------|-------------| +| **AssetBundle build output** | The output path of an AssetBundle build | +| **Addressables folder** | `StreamingAssets/aa` folder from a Player build, including BuildLayout files | +| **Entities content** | `StreamingAssets/ContentArchives` folder for [Entities](https://docs.unity3d.com/Packages/com.unity.entities@1.4/manual/content-management-intro.html) projects | +| **Player Data folder** | The `Data` folder of a Unity Player build | +| **Compressed Player builds** | The `data.unity3d` file will be analyzed like AssetBundles | +| **BuildReport files** | The build report is typically found at a path like `Library/LastBuild.buildreport`and is a binary serialized file | +| **AssetDatabase Artifacts** | The tool will work to some extent with serialized files created in the AssetDatabase artifact storage, inside the Library folder | + +> **Note**: Some platforms require extracting content from platform-specific containers first (e.g., `.apk` files on Android). + +--- + +## Output Database + +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.md) for the database schema reference and information about extending this command.** + +--- + +## Troubleshooting + +### File Loading Warnings + +``` +Failed to load 'C:\....\MyData.db'. File may be corrupted or was serialized with a newer version of Unity. +``` + +These warnings occur when the tool encounters non-Unity files in the analyzed directory. They are usually harmless—the analyze process continues and produces a valid database. + +**Solutions:** +- Use `-p "*.bundle"` to filter by file extension +- Use `--no-recurse` to limit directory depth +- Use `-v` (verbose) to see which files are ignored + +The tool automatically ignores common non-Unity file types (`.txt`, `.json`, `.manifest`, etc.). + +### TypeTree Errors + +``` +Error processing file: C:\...\TestProject_Data\level0 +System.ArgumentException: Invalid object id. +``` + +This error occurs when SerializedFiles are built without TypeTrees. The command will skip these files and continue. + +**Solution:** Enable **ForceAlwaysWriteTypeTrees** in your Unity build settings. See [Unity Content Format](../../Documentation/unity-content-format.md) for details. + +### SQL Constraint Errors + +``` +SQLite Error 19: 'UNIQUE constraint failed: objects.id' +``` +or +``` +SQLite Error 19: 'UNIQUE constraint failed: serialized_files.id'. +``` + +These errors occur when the same serialized file name appears in multiple sources: + +| Cause | Solution | +|-------|----------| +| Multiple builds in same directory | Analyze each build separately | +| Scenes with same filename (different paths) | Rename scenes to be unique | +| AssetBundle variants | Analyze variants separately | + +See [Comparing Builds](../../Documentation/comparing-builds.md) for strategies to compare different versions of builds. + +### Slow Analyze times, large output database + +Consider using the `--skip-references` argument. + +A real life analyze of a big Addressables build shows how large a difference this can make: + +* 208 seconds and producted a 500MB database (not specifying --skip-reference) +* 9 seconds and produced a 68 MB file (with --skip-reference) + +The references are not needed for core asset inventory and size information. + +Note: When specifying `--skip-reference` some functionality is lost: + +* the `find-refs` command will not work +* `view_material_shader_refs` and `view_material_texture_refs` will be empty +* Queries that look at the relationship between objects will not work. For example the refs table is required to link between a `MonoBehaviour` and its `MonoScript`. +* The `objects.crc32` column will be NULL/0 for all objects. This means: + * No detection of identical objects by content hash (See [Comparing Builds](../../Documentation/comparing-builds.md)) + * The `view_potential_duplicates` view relies partially on CRC32 to distinguish true duplicates + +Future work: The refs table contains a lot of repeated strings and could be made smaller and more efficient. It might also be prudent to control the CRC32 calculation using an independent flag. + diff --git a/Documentation/command-archive.md b/Documentation/command-archive.md new file mode 100644 index 0000000..a563926 --- /dev/null +++ b/Documentation/command-archive.md @@ -0,0 +1,73 @@ +# archive Command + +The `archive` command provides utilities for working with Unity Archives (AssetBundles and web platform `.data` files). + +## Sub-Commands + +| Sub-Command | Description | +|-------------|-------------| +| [`list`](#list) | List contents of an archive | +| [`extract`](#extract) | Extract contents of an archive | + +--- + +## list + +Lists the SerializedFiles contained within an archive. + +### Quick Reference + +``` +UnityDataTool archive list +``` + +### Example + +```bash +UnityDataTool archive list scenes.bundle +``` + +--- + +## extract + +Extracts the contents of an archive to disk. This is similar to Unity's `WebExtract` tool. + +### Quick Reference + +``` +UnityDataTool archive extract [options] +``` + +| Option | Description | Default | +|--------|-------------|---------| +| `` | Path to the archive file | *(required)* | +| `-o, --output-path ` | Output directory | `archive` | + +### Example + +```bash +UnityDataTool archive extract scenes.bundle -o contents +``` + +**Output files:** +``` +contents/BuildPlayer-SampleScene.sharedAssets +contents/BuildPlayer-SampleScene +contents/BuildPlayer-Scene2.sharedAssets +contents/BuildPlayer-Scene2 +``` + +> **Note:** The extracted files are binary SerializedFiles, not text. Use the [`dump`](command-dump.md) command to convert them to readable text format. + +--- + +## Comparison: extract vs dump + +| Command | Output | Use Case | +|---------|--------|----------| +| `archive extract` | Binary SerializedFiles, .resS anything else inside the archive content | When you need all the raw files inside an archive | +| `dump` | text | When you want to inspect object content | + +The `dump` command can directly process archives without extracting first. + diff --git a/Documentation/command-dump.md b/Documentation/command-dump.md new file mode 100644 index 0000000..d23b7f2 --- /dev/null +++ b/Documentation/command-dump.md @@ -0,0 +1,111 @@ +# dump Command + +The `dump` command converts Unity SerializedFiles into human-readable text format. This is useful for inspecting the internal structure and properties of Unity assets. + +## Quick Reference + +``` +UnityDataTool dump [options] +``` + +| Option | Description | Default | +|--------|-------------|---------| +| `` | Path to file to dump | *(required)* | +| `-o, --output-path ` | Output folder | Current folder | +| `-f, --output-format ` | Output format | `text` | +| `-s, --skip-large-arrays` | Skip dumping large arrays | `false` | +| `-i, --objectid ` | Only dump object with this ID | All objects | + +## Examples + +Dump all objects in a file to the current directory: +```bash +UnityDataTool dump /path/to/file +``` + +Dump to a specific output folder: +```bash +UnityDataTool dump /path/to/file -o /path/to/output +``` + +Dump a single object by ID: +```bash +UnityDataTool dump /path/to/file -i 1234567890 +``` + +Skip large arrays for cleaner output: +```bash +UnityDataTool dump /path/to/file -s +``` + +--- + +## Archive Support + +When you pass an Archive file (like an AssetBundle), the command dumps all SerializedFiles inside. + +**Example:** For an AssetBundle `scenes.bundle` containing two scenes: + +```bash +UnityDataTool dump scenes.bundle +``` + +**Output files:** +``` +BuildPlayer-SampleScene.sharedAssets.txt +BuildPlayer-SampleScene.txt +BuildPlayer-Scene2.sharedAssets.txt +BuildPlayer-Scene2.txt +``` + +--- + +## Output Format + +The output is similar to Unity's `binary2text` tool. Each file begins with external references: + +``` +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/..." GUID: 00000000000000000000000000000000 Type: 0 +``` + +Followed by object entries: + +``` +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 +``` + +**Refer to the [TextDumper documentation](textdumper.md) for detailed output format explanation.** + +--- + +## Understanding PPtrs + +PPtrs (Property Pointers) are Unity's mechanism for referencing objects: + +| Field | Description | +|-------|-------------| +| `m_FileID` | Index into External References list (0 = same file) | +| `m_PathID` | Object's Local File Identifier (LFID) in that file | + +A null reference will have value m_FileID = 0, m_PathID = 0 + +The external reference table is used to resolve cross-file references. It always starts at index 1. m_FileID 0 is used for references within the current file. + + + diff --git a/Documentation/command-find-refs.md b/Documentation/command-find-refs.md new file mode 100644 index 0000000..3e44fe2 --- /dev/null +++ b/Documentation/command-find-refs.md @@ -0,0 +1,97 @@ +# find-refs Command + +> ⚠️ **Experimental:** This command may not work as expected in all cases. + +The `find-refs` command traces reference chains leading to specific objects. Use it to understand why an asset was included (and potentially duplicated) in a build. + +## Quick Reference + +``` +UnityDataTool find-refs [options] +``` + +| Option | Description | Default | +|--------|-------------|---------| +| `` | Path to database from `analyze` command | *(required)* | +| `-i, --object-id ` | ID of object to analyze (from `id` column) | — | +| `-n, --object-name ` | Name of objects to analyze | — | +| `-t, --object-type ` | Type filter when using `-n` | — | +| `-o, --output-file ` | Output filename | — | +| `-a, --find-all` | Find all chains instead of stopping at first | `false` | + +> **Note:** Either `--object-id` or `--object-name` must be provided. + +## Prerequisites + +This command requires a database created by the [`analyze`](command-analyze.md) command **without** the `--skip-references` option. + +--- + +## Examples + +Find references to an object by name and type: +```bash +UnityDataTool find-refs my_database.db -n "MyTexture" -t "Texture2D" -o refs.txt +``` + +Find references to a specific object by ID: +```bash +UnityDataTool find-refs my_database.db -i 12345 -o refs.txt +``` + +Find all duplicate references (useful for finding why an asset is duplicated): +```bash +UnityDataTool find-refs my_database.db -n "SharedMaterial" -t "Material" -a -o all_refs.txt +``` + +--- + +## Use Cases + +| Scenario | Approach | +|----------|----------| +| **Why is this asset included?** | Use `-n` with the asset name | +| **Why is this asset duplicated?** | Use `-n` to find all instances (same name, different IDs) | +| **Trace specific object** | Use `-i` with the object ID from the database | +| **Find all reference chains** | Add `-a` flag (may take longer) | + +--- + +## Output Format + +The output shows the reference chain from root assets to your target object: + +``` +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 + ... + Transform (id=1234) [Component of MyButtonEffectLayer (1) (id=938)] + +Analyzed 266 object(s). +Found 1 reference chain(s). +``` + +### Reading the Output + +| Element | Description | +|---------|-------------| +| `↓ property_name` | The property containing the reference | +| `[Component of X (id=Y)]` | Shows the GameObject for Components | +| `[Script = X]` | Shows the script name for MonoBehaviours | + +**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/comparing-builds.md b/Documentation/comparing-builds.md new file mode 100644 index 0000000..b13e862 --- /dev/null +++ b/Documentation/comparing-builds.md @@ -0,0 +1,330 @@ +# Comparing Builds + +This topic gives examples using several tools and techniques to compare build output files. + +The tools used in this topic are as follows: + +* [comparebuilds.ps1](../Scripts/comparebuilds.ps1) An example script using UnityDataTool to compare two builds at the object level +* [comparebundles.ps1](../Scripts/comparebundles.ps1) An example script using UnityDataTool to compare two versions of an AssetBundle. +* A diff tool for comparing directories, binary files and text files. These tools are readily available on Windows, Mac and Linux. For example Beyond Compare, WinMerge, and kdiff3. +* WebExtract. A tool for extracting contents of an Unity Archive file (WebExtract is included in the Unity Editor installation). +* `UnityDataTool dump`. A feature of UnityDataTool that create a text representation of the content of a Unity Serialized File. +* binary2text. A tool very similar to UnityDataTool dump. It is slower but has a few features that are not yet exposed by UnityDataTool. (binary2text is included in the Unity Editor installation). + +The [Overview of Unity Content](./unity-content-format.md) topic gives useful background for the file formats involved when comparing builds. + +## Why Compare Builds? + +When working with Unity a project will typically be rebuilt many times, as part of testing or as new content is released. Each time a build is performed it is likely that some content will changed. Normally the change should be predictable, based on changes made to assets, scenes, scripts, packages or upgrades to the Unity Editor. When nothing has changed at all the build output should be exactly the same as the previous build. + +Sometimes the change in a build may be unexpected, for example if there is a significant change in the size of the output, or when many AssetBundles have new content hashes after what was expected to be a small routine rebuild. So we may want to compare different versions of a build to try to understand why a large output change has occurred. + +A particular important reason for comparing builds is when there seems to be a problem of **non-determinism**. Non-determinism means that the build output changes when rebuilt, even when nothing has changed in the source project. Sometimes this is observed on the same machine, or sometimes when built on different machines even when they have similar operating system and hardware. For some projects this is not a problem, but any non-determinism can be problematic for the creation of clean and efficient build pipelines and release and distribution processes. Often these problems can be attributed back to code running in build callbacks or Awake methods, including code in 3rd party packages. Some texture compression algorithms are known to be nondeterministic. It may also be caused because certain critical files are not included in source control. However in some rare cases this require a bug report back to Unity. The techniques described here can pinpoint the source of the non-determinism and help finding a resolution. + +When comparing builds there may be a huge number of individual differences. The main goal is to try to classify the changes down enough to give some insight into the root causes to see if these changes are "expected" and if they can be avoided. For example: + + * That a change to a single global setting has impacted the variants of all the Shaders. + * That a change adding new fields to a widely used MonoBehaviour has changed many scenes and prefabs. + * That a single asset is duplicated in many AssetBundles, so changing it has impacted all of them. + +## AssetBundles and Player Builds + +This topic focuses on AssetBundles, because most questions about changes in a build come up around AssetBundles. However the same techniques can also be used to analyze player builds (so long as the player is built with [TypeTrees enabled](./unity-content-format.md#enabling-typetrees-in-the-player)). + +# Example 1 - Changes to serialized values in an Addressables build + +AssetBundles contain the serialized state of objects. If objects are added or removed then obviously the AssetBundle will change. But another common cause of changes in an AssetBundle is when the serialized values of objects change. + +For example, consider a build using the Addressables package that just builds a small Prefab. The Prefab includes two GameObjects, plus a MonoBehavior Component that is an instance of this class. + +``` +using UnityEngine; + +public class MyMonoBehaviour : MonoBehaviour +{ + public int Foo = 42; + public string Bar = "Hello, World!"; +} +``` + +A first build is made that outputs two AssetBundles (into the Addressables location inside the Library folder). A copy of this build is created by saving it into a directory called `Build1`. + +Then the Prefab is edited to apply two small changes: + +* the position on the Tranform component of one of the GameObjects is changed +* the value of the two fields on the MyMonoBehaviour component are changed. + +Finally a second build is performed, and the output saved into the `Build2` directory. + +We will use tools to compare the two builds. + +## Diff Comparison + +A quick way to compare two builds is to do a file-level comparison, e.g. using a diff tool such as `WinMerge` to compare build1 and build2. + +![](./AddressableBuildComparison.png) + +This will quickly narrow down which AssetBundle files have changed. But AssetBundles files are binary archive files, so this won't show what has changed **inside** the files. + +Addressables builds, by default, include the content hash as part of the file name. This means that the entire file name changes when the content changes. So in this case the changes to the prefab result in `group1_assets_all_8b203c492180c6e373f3632682c6edea.bundle` being renamed to `group1_assets_all_b7bd0b63663f8f889fc4228ba0722711.bundle`. This makes it harder to match up the equivalent files in comparison tools. But in this very simple case its not hard to see that the file was renamed because of a change in the hash. + +>[!TIP] +>The hash can be removed from the bundle file names, by changing a Group-level preference in Addressables to specify "No Hash". The "No Hash" option can make it easier to compare and work with the output from Addressables builds and can be a better setting to use overall for many projects. However, because hashes are the default, this example uses that naming convention. + +![](./RemoveHashName.png) + +## UnityDataTool object comparison + +UnityDataTools does not natively support comparing two builds. But we can do it by analyzing each build individually, into two SQLite databases, then running cross-database queries to compare the contents of the two builds. + +For example, two database could be generated as follows: + +``` +UnityDataTool.exe analyze -o build1.db .\Build1\ +UnityDataTool.exe analyze -o build2.db .\Build2\ +``` + +The [comparebuilds.ps1](../Scripts/comparebuilds.ps1) script is an example PowerShell script that prints a comparison of each object. In this case the output would be something like this: + + +``` +asset_bundle object_id type name status size_build1 size_build2 crc32_build1 crc32_build2 +------------------------------------------------------------ -------------------- ------------- --------------------------------------------------- --------- ----------- ----------- ------------ ------------ +cee5ebdcaa450bf0baa84e3f6fc812e5_monoscripts_d8d9cc26b795c2d 1 AssetBundle cee5ebdcaa450bf0baa84e3f6fc812e5_monoscripts.bundle Same 164 164 3099751197 3099751197 +97b5823f9e593f7d3.bundle + +cee5ebdcaa450bf0baa84e3f6fc812e5_monoscripts_d8d9cc26b795c2d 6985605818965764450 MonoScript MyMonoBehaviour Same 84 84 3996385875 3996385875 +97b5823f9e593f7d3.bundle + +group1_assets_all_8b203c492180c6e373f3632682c6edea.bundle -7417806626306848847 GameObject GameObject Same 51 51 4246140359 4246140359 + +group1_assets_all_8b203c492180c6e373f3632682c6edea.bundle 7800266938829834161 Transform Same 68 68 934917823 934917823 + +group1_assets_all_8b203c492180c6e373f3632682c6edea.bundle 5404940467391498161 MonoBehaviour Different 56 56 843380543 2454703857 + +group1_assets_all_8b203c492180c6e373f3632682c6edea.bundle -3130111841077557327 Transform Same 92 92 4113599061 4113599061 + +group1_assets_all_8b203c492180c6e373f3632682c6edea.bundle -1433429198062811215 GameObject MyPrefab Same 35 35 1577821869 1577821869 + +group1_assets_all_8b203c492180c6e373f3632682c6edea.bundle 7070054886790006705 Transform Different 68 68 1402962886 1895291976 + +group1_assets_all_8b203c492180c6e373f3632682c6edea.bundle -350173738715183183 GameObject GameObject2 Same 39 39 4260572786 4260572786 + +group1_assets_all_8b203c492180c6e373f3632682c6edea.bundle 1 AssetBundle 15bea74e638d7d4118f8b0a23ddac6b6.bundle Same 324 324 2773319152 2773319152 + +``` + +Changes are detected by matching up the objects from each database and comparing their CRC and size values. For large builds the output would be very verbose, but for the purpose of this example it confirms that only one Transform and the MonoBehaviour have changed between the two builds. + +The script also supports printing out objects that only exist in one of the two builds (e.g. objects that have been added or removed). + +### Comparing Individual AssetBundles + +A variation of comparing entire builds is to compare two versions of an individual AssetBundle. + +The script [comparebundles.ps1](../Scripts/comparebundles.ps1) is an example of this approach. It creates temporary sqlite databases, so that the comparison is a convenient one-step process. + +Running +``` +comparebundles.ps1 .\Build1\group1_assets_all_8b203c492180c6e373f3632682c6edea.bundle .\Build2\group1_assets_all_b7bd0b63663f8f889fc4228ba0722711.bundle +``` + +Will output: + +``` +serialized_file object_id type name status size_build1 size_build2 crc32_build1 crc32_build2 +------------------------------------ ------------------- ------------- ---- --------- ----------- ----------- ------------ ------------ +CAB-a39d6b841dbaf92054f65846f0e62c2a 5404940467391498161 MonoBehaviour Different 56 56 2646584758 378564635 +CAB-a39d6b841dbaf92054f65846f0e62c2a 7070054886790006705 Transform Different 68 68 2385093893 2906782347 +``` + +This really narrows things down to the two objects that actually changed. But it doesn't explain what changed inside each object. + +## Comparing Serialized File content + +To actually see precisely what changed we need to look at the content of the SerializedFiles inside the AssetBundles. The UnityDataTool can be used to dump the content of the SerializedFiles into a human-readable format, similar to the `binary2text` tool. + +For example: + +``` +UnityDataTool dump .\Build1\group1_assets_all_8b203c492180c6e373f3632682c6edea.bundle -o .\Build1 +``` + +This finds the Serialized Files inside the AssetBundle. In this case its a single file with a name like "CAB-....."). UnityDataTool reads the file and produced a text representation in the specified output directory ("Build1/CAB-a39d6b841dbaf92054f65846f0e62c2a.txt"). + +The same process can be repeated for the equivalent AssetBundle in the second build directory. + +The files include an exhaustive dump of all the serialized data for all the objects. In this simple case there only a few objects, so the file is relatively small. + +The exact differences are easiest to see by using a diff tool: + +![](./AddressableSerializedFileComparison.png) + +As expected, it's only fields on the MonoBehaviour and one of the Transforms that have new values. + +In a normal case we would not know exactly what changed in the build, so rather than confirming the expected changes we probably would be working in the other direction - using comparison technique to see which objects and values changed and try to build an understanding of whether this is "expected" or not. + +Note: When you know the id of the object that has changed you can pass the `--objectid` argument to the `dump` command to only dump that object. This can be useful for large Serialized Files with many objects, where you want to focus on changes to a single object. + +# Example 2 - Changes in a texture + +The previous example covers the common case where the changes are limited to serialized values directly inside Serialized Files. However its also common that data inside the auxiliary .resS and .resource files can change, based on changes to textures, meshes, audio or video. This can also cause the AssetBundle content to change, even if the Serialized Files themselves are unchanged. + +As an example of that case, suppose we have a non-Addressable AssetBundle build that includes an AssetBundle called "sprites.bundle". It contains 3 textures ("red.png", "Snow.png" and "Snow 1.png"). The pixels of the "red.png" texture has been changed between the two builds, while the other two textures are unchanged. + +As in example 1, we put the two snapshot copies of the AssetBundles into the folder Build1 and Build2. + +Note: These AssetBundles were originally built into a folder named "AssetBundles" so the output also includes an "Manifest Bundle" named `AssetBundle` that contains the AssetBundleManifest object. See [AssetBundle file format reference](https://docs.unity3d.com/6000.1/Documentation/Manual/assetbundles-file-format.html) for details. + +## File-level comparison + +A diff tool can be used to compare the build output in build1 and build2. + +![](./AssetBundleBuildComparison.png) + +This shows that only two AssetBundle have changed, the sprites.bundle and the manifest bundle ("AssetBundles"). + +## UnityDataTool object comparison + +To compare the full builds, UnityDataTool can be used to generate two SQLite databases, one for each build. + +``` +UnityDataTool.exe analyze -o build1.db .\Build1\ +UnityDataTool.exe analyze -o build2.db .\Build2\ +``` + +Then the [comparebuilds.ps1](../Scripts/comparebuilds.ps1) script can be run. + +This is a truncated example output: + + +``` +asset_bundle object_id type name status size_build1 size_build2 crc32_build1 crc32_build2 +-------------- -------------------- ------------------- ------------------- --------- ----------- ----------- ------------ ------------ +AssetBundles 1 AssetBundle Same 104 104 241569179 241569179 +AssetBundles 2 AssetBundleManifest AssetBundleManifest Different 184 184 4124235088 3102991602 +audio.bundle -1630896013228033972 AudioClip audio Same 18656 18656 883020518 883020518 +audio.bundle 1 AssetBundle audio.bundle Same 144 144 2644028121 2644028121 +sprites.bundle -4266742476527514910 Sprite Snow 1 Same 464 464 2360191667 2360191667 +sprites.bundle -39415655269619539 Texture2D Snow 1 Same 524496 524496 3893000759 3893000759 +sprites.bundle -3600607445234681765 Texture2D red Different 152079 152079 3533099562 3115177070 +sprites.bundle -1350043613627603771 Texture2D Snow Same 524492 524492 3894005184 3894005184 +sprites.bundle 1 AssetBundle sprites.bundle Same 460 460 245831303 245831303 +``` + +The output pinpoints that "red" has changed. The AssetBundleManifest object also changes, which is expected because it lists AssetBundle content hashes. + +>[!TIP] +>The object size reported by UnityDataTool includes the size of data inside .resS and .resource files when that is referenced by that object. + + +### Comparing Individual AssetBundles + +To focus in on sprites.bundle the [comparebundles.ps1](../Scripts/comparebundles.ps1) script can be used. + +``` +comparebundles.ps1 .\Build1\sprites.bundle .\Build2\sprites.bundle +``` + +The output from this example would be: + + +``` +serialized_file object_id type name status size_build1 size_build2 crc32_build1 crc32_build2 +------------------------------------ -------------------- --------- ---- --------- ----------- ----------- ------------ ------------ +CAB-6b49068aebcf9d3b05692c8efd933167 -3600607445234681765 Texture2D red Different 152079 152079 3533099562 3115177070 +``` + +### Analyzing Differences in .ResS Files + +UnityDataTool helps pinpoint which AssetBundle objects have changed between builds. But to actually understand "what" has changed it is necessary to look deeper into the content of the AssetBundles and how Unity serializes data. + +We already know that sprites.bundle has changed between builds, and the script pinpoints "red" as the object that changed, whereas "Snow" and "Snow 1" are unchanged. So how can we determine more information about what has changed in the build of "red.png"? + +In example 1 we took a bit of a shortcut and went straight to comparing Serialized Files. But AssetBundles can also contain other files, including .resS files with the actual texture content. So we need to look more broadly at the content of the AssetBundle. + +The **WebExtract** tool that is shipped with Unity can be used to expand the content of the AssetBundle (which is an Archive file). When run on an sprites.bundle it creates a subdirectory with all the contents of the AssetBundle expanded as individual files. + +``` +cd Build1 +WebExtract.exe sprites.bundle +cd ..\Build2 +WebExtract.exe sprites.bundle +``` + +Then a diff tool can be used to compare the contents of the AssetBundle: + +![](./AssetBundleContentComparison.png) + +This is a diagram showing the relationship between those files and the original AssetBundle: + +![](./SpritesBundleContent.png) + +Based on the diff, we see that the SerializedFile is unchanged between builds, but the .resS file is different. This means that the Texture2D object has the exact same properties (including dimensions, format etc), but the pixel data is different. + +For the sake of further illustration, we can go deeper and look at how the .resS file relates to the 3 textures in sprites.bundle. + +When a binary diff is performed on the two verions of the .resS file we can see that all the differences are located near the start of the file, finishing before address 0x25150 (151,888 in decimal). The rest of the file is identical. + +![](./ResFileBinaryDiff.png) + +We know from our UnityDataTool queries that "red" is the only texture that changed, so we can surmise that the "red" texture is at the start of the .resS file. Its possible to confirm this by further analysis of the AssetBundle contents. + +To understand the content of a resS file we have to look at the associated SerializedFile. E.g. to understand what is contained inside `CAB-6b49068aebcf9d3b05692c8efd933167.resS` we need to look inside `CAB-6b49068aebcf9d3b05692c8efd933167`. + +Because the SerializedFile is a binary format, we first need to convert it to text. We can do this using the `dump` feature of UnityDataTools. We can run this on the WebExtract output from either build1 or build2 (because the file is identical from both builds). + +``` +UnityDataTool dump CAB-6b49068aebcf9d3b05692c8efd933167 +``` + +Inside this file we can search for all mentions of "CAB-6b49068aebcf9d3b05692c8efd933167.resS". This search discovers 3 Texture2D objects. These are the relevant parts of the output file: + +``` +ID: -3600607445234681765 (ClassID: 28) Texture2D + m_Name (string) red + ... + m_StreamData (StreamingInfo) + offset (UInt64) 0 + size (unsigned int) 151875 + path (string) archive:/CAB-6b49068aebcf9d3b05692c8efd933167/CAB-6b49068aebcf9d3b05692c8efd933167.resS +``` + +``` +ID: -1350043613627603771 (ClassID: 28) Texture2D + m_Name (string) Snow + ... + m_StreamData (StreamingInfo) + offset (UInt64) 151888 + size (unsigned int) 524288 + path (string) archive:/CAB-6b49068aebcf9d3b05692c8efd933167/CAB-6b49068aebcf9d3b05692c8efd933167.resS +``` + +``` +ID: -39415655269619539 (ClassID: 28) Texture2D + m_Name (string) Snow 1 + ... + m_StreamData (StreamingInfo) + offset (UInt64) 676176 + size (unsigned int) 524288 + path (string) archive:/CAB-6b49068aebcf9d3b05692c8efd933167/CAB-6b49068aebcf9d3b05692c8efd933167.resS + +``` + +The resS file is a simple format with no header. It is literally just the binary data of textures or meshes, concatenated together (sometimes with extra padding bytes between entries). The m_StreamData describes each range of bytes inside the .resS file. The total file size on disk is 1200463 bytes, so every byte of the file is accounted for based on the three objects. + +Based on this analysis we have confirmed that the range information for "red" exactly matches the changes we observed in the binary diff. We also have a detailed understanding of how the data is structured, as summarized in this diagram: + +![](./SpritesBundleDetailedContent.png) + +This confirms our understanding that pixel data inside "red.png" is what caused the AssetBundle content to change. + +>[!TIP] +>This same approach can be used to analyze mesh data (which can be stored alongside textures in .resS files). And for Audio and Video data inside .resource files. + +# Special cases + +In some rare cases the binary Serialized File is different between two builds, but the text "dump" is identical. + +* This can happen if the change happens in the header of the file, or in some padding bytes. Such cases are rare because the Serialized File format is quite stable, but it has happened when performance or stabilities improvements have been introduced that changed the header or padding. +* Sometimes float or double values might appear to be identical in the text representation, but there could be a difference in the actual binary representation. binary2text has a "-hexfloat" argument that addresses this issue. \ No newline at end of file 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 new file mode 100644 index 0000000..20f0f82 --- /dev/null +++ b/Documentation/unity-content-format.md @@ -0,0 +1,115 @@ +# Overview of Unity Content + +This section gives an overview of the core Unity file types and how they are used in different types of builds. It also covers the important concept of "TypeTrees". This gives context for understanding what UnityDataTools can and cannot do. + +## File Formats + +### SerializedFile + +A SerializedFile the name used for Unity's binary file format for serializing objects. It is made up of a file header, +then each Object, serialized one after another. This binary format is also available in the Editor, but typically Editor content uses the Unity YAML format instead. + +The SerializedFiles in build output represent the project content, but optimized for the target platform. Unity will combine objects from multiple source assets together into files, exclude certain objects (for example editor-only objects), and potentially split or duplicate objects across multiple output files. This arrangement of objects is called the `build layout`. Because of all this transformation, there is not a one-to-one mapping between the source assets and the SerializedFiles in the build output. + +### Unity Archive + +An Unity Archive is a container file (similar to a zip file). Unity can `mount` this file, which makes the files inside it visible to Unity's loading system, via the Unity "Virtual File System" (VFS). Unity Archives often apply compression to the content, but it is also possible to create an uncompressed Archive. + +## AssetBundles + +[AssetBundles](https://docs.unity3d.com/Manual/AssetBundlesIntro.html) use the Unity Archive file format, with conventions for what to expect inside the archive. The [Addressables](https://docs.unity3d.com/Manual/com.unity.addressables.html) package uses AssetBundles, so its build output is also made up of Unity Archive files. + +AssetBundles always contain at least one SerializedFile. In the case of an AssetBundle containing Scenes there will be multiple Serialized Files. AssetBundles can also contain auxiliary files, such as .resS files containing Textures and Meshes, and .resource files containing audio or video. + +UnityDataTools supports opening Archive files, so it is able to analyze AssetBundles. + +## Player Builds + +A player build produces content as well as compiled code (assemblies, executables) and various configuration files. UnityDataTool only concerns itself with the content portion of that output. + +The content compromises of the scenes in the Scene List, the contents of Resources folders, content from the Project Preferences (the "GlobalGameManagers") and also all Assets referenced from those root inputs. This translates into SerializedFiles in the build output. + +The SerializedFiles are named in a predictable way. This is a very quick summary: + +* Each scene in the SceneList becomes a "level" file, e.g. "level0", "level1". +* Assets referenced from Scenes becomes "sharedAssets" files, e.g. "sharedAssets0.assets", "sharedAssets1.assets". Scenes are processed in order of the scene list and assets are stored in the sharedasset file corresponding to the scene where they are first encountered. This means that a level file may reference multiple sharedasset files, but only the ones at the same number and lower. For example the 3rd scene, level2 can reference "sharedAssets2.assets", "sharedAssets1.assets" and "sharedAssets0.assets" but never "sharedAssets3.assets". +* The contents of the Resources folder becomes "resources.assets". +* The Preferences become "globalgamemanager". Assets referenced from "globalgamemanager" are saved in "globalgamemanager.assets". + +If [compression](https://docs.unity3d.com/6000.2/Documentation/ScriptReference/BuildOptions.CompressWithLz4HC.html) is enabled, the Player build will compress all the serialized files into a single Unity Archive file, called `data.unity3d`. + +### Enabling TypeTrees in the Player + +UnityDataTools supports Player build output, because that uses the same SerializedFiles and Archives that AssetBundles use. But often its output is not very useful. That is because, by default, Player builds do not include TypeTrees. + +>[!IMPORTANT] +>It is possible to generate TypeTrees for the Player data, starting in Unity 2021.2. +>This makes that output compatible with UnityDataTool, but it is not a recommended flag to enable for your production builds. + +To do so, the **ForceAlwaysWriteTypeTrees** Diagnostic Switch must be enabled in the Editor Preferences (Diagnostics->Editor section). + +![](./TypeTreeForPlayer.png) + + +Note: The `Resources\unity default resources` file is shipped with the Unity Editor and is not rebuilt when doing a Player Build. It does not have TypeTrees. Hence it is normal that this file emits errors when analyzing a player build, even after rebuilding with TypeTrees enabled. For example: + +``` +Error processing file: C:\TestProject\CompressedPlayer\TestProject_Data\Resources\unity default resources +System.ArgumentException: Invalid object id. +``` + +For more information about TypeTrees see the following section. + +## TypeTrees + +The TypeTree is a data structure exposing how objects have been serialized, i.e. the name, type and +size of their properties. It is used by Unity when loading an SerializedFile that was built by a +previous Unity version. When Unity is deserializing an object it needs to check if the current Type +definition exactly matches the Type definition used when the object was serialized. If they do not match +Unity will attempt to match up the properties as best as it can, based on the property names and structure +of the data. This process is called a "Safe Binary Read" and is somewhat slower than the regular fast binary read path. + +TypeTrees are important in the case of AssetBundles, to avoid rebuilding and redistributing all AssetBundles after each minor upgrade of Unity or after doing minor changes to your MonoBehaviour and ScriptableObject serialization. However there can be a noticeable overhead to storing the TypeTrees in each AssetBundle, e.g. the header size of each SerializedFile is bigger. + +TypeTrees also make it possible to load an AssetBundle in the Editor, when testing game play. + +>[!NOTE] +>There is a flag available when building AssetBundles that will exclude TypeTrees, see [BuildAssetBundleOptions.DisableWriteTypeTree](https://docs.unity3d.com/6000.2/Documentation/ScriptReference/BuildAssetBundleOptions.DisableWriteTypeTree.html). This has implications for future redistribution of your content, so use this flag with caution. + +For Player Data the expectation is that you always rebuild all content together with each new build of the player. +So the Assemblies and serialized objects will all have matching types definitions. That is why, by default, the types are not included. + +UnityDataTools relies on TypeTrees in order to understand the content of serialized objects. Using this approach it does +not need to hard code any knowledge about what exact types and properties to expect inside each built-in Unity type +(for example Materials and Transforms). And it can interpret serialized C# classes (e.g. MonoBehaviours, ScriptableObjects +and objects serialized through the SerializeReference attribute). That also means that UnityDataTools cannot understand +Player built content, unless the Player was built with TypeTrees enabled. + +>[!TIP] +>The `binary2text` tool supports an optional argument `-typeinfo` to enable dumping out the TypeTrees in a SerializedFile header. That is a useful way to learn more about TypeTrees and to see exactly how Unity data is represented in the binary format. + +### Platform details for using UnityDataTool with Player Data + +The output structure and file formats for a Unity Player build are quite platform specific. + +On some platforms the content is packaged into platform-specific container files, for example Android builds use .apk and .obb files. So accessing the actual SerializedFiles may involve mounting or extracting the content of those files, and possibly also opening a data.unity3d file inside them. + +UnityDataTools directly supports opening the .data container file format used in Player builds that target Web platforms (e.g. WebGL). Specifically the "archive list" and "archive extract" command line option works with that format. Once extracted you can run other UnityDataTool commands on the output. + +Android APK files are not difficult to open and expand using freely available utilities. For example on Windows they can be opened using 7-zip. Once the content is extracted you can run UnityDataTool commands on the output. + +## Mapping back to Source Assets + +Because Unity rearranges objects in the build into a build layout there is no 1-1 mapping between the output files and the original source assets. Only Scene files have a pretty direct mapping into the build output. + +The UnityDataTool only looks at the output of the build, and has no information available about the source paths. This is expected, because the built output is optimized for speed and size, and there is no need to "leak" a lot of details about the source project in the data that gets shipped with the Player. + +However in cases where you want to understand what contributes to the size your build, or to confirm whether certain content is actually included, then you may want to correlate the output back to the source assets in your project. + +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 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. + +`UnityDataTools analyze` can also import Addressables build layout files, which include source asset information. See [Addressable Build Reports](./addressables-build-reports.md). + +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 fc6b3b7..4a20384 100644 --- a/README.md +++ b/README.md @@ -1,100 +1,87 @@ # UnityDataTools -The UnityDataTool is a set of command line tools showcasing what can be done with the -UnityFileSystemApi native dynamic library. The main purpose of these tools is to analyze the -content of Unity data files. You can directly jump -[here](https://github.com/Unity-Technologies/UnityDataTools/blob/main/UnityDataTool/README.md) -if your goal is to understand how to use the UnityDataTool command-line tool. - -The UnityFileSystemApi library is distributed in the Tools folder of the Unity editor (starting in -version 2022.1.0a14). For simplicity, it is also included in this repository. The library is -backward compatible, which means that it can read data files generated by any previous version of -Unity. - -Note that the UnityFileSystemApi library included in this repository is a custom version containing -an additional function that is required to support the -[SerializeReference](https://docs.unity3d.com/ScriptReference/SerializeReference.html) -attribute. This version of the library will be included in future releases of Unity. - - -## What is the purpose of the UnityFileSystemApi native library? - -The purpose of the UnityFileSystemApi is to expose the functionalities of the WebExtract and -binary2text tools, but in a more flexible way. To fully understand what it means, let's first -discuss how Unity generates the data files in a build. The data referenced by the scenes in a build -is called the Player Data and is contained in SerializedFiles. A SerializedFile is the file format -used by Unity to store its data. In builds, they contain the serialized assets in the target's -platform-specific format. - -When using AssetBundles or Addressables, things are slightly different. Firstly, note that -Addressables are AssetBundles on disk so we will only use the term AssetBundle in the remaining of -this document. AssetBundles are archive files (similar to zip files) that can be mounted at -runtime. They contain SerializedFiles, but contrary to those of the Player Data, they include what -is called a TypeTree[1](#footnote1). - -> Note: it is possible to generate TypeTrees for the Player data starting in Unity 2021.2. -> To do so, the *ForceAlwaysWriteTypeTrees* Diagnostic Switch must be enabled in the Editor -> Preferences (Diagnostic/Editor section). - -The TypeTree is a data structure exposing how objects have been serialized, i.e. the name, type and -size of their properties. It is used by Unity when loading an AssetBundle that was built by a -previous Unity version (so you don't necessarily have to update all AssetBundles after upgrading a -project to a newer version of Unity). - -The content of a SerializedFile including a TypeTree can be converted to a human-readable format -using the binary2text tool that can be found in the Tools folder of Unity. In the case of -AssetBundles, the SerializedFiles must first be extracted using the WebExtract tool that is also in -the Tools folder. For the Player Data, there is no TypeTree because it is included in a build and - -The text file generated by binary2text can be very useful to -diagnose issues with a build, but they are usually very large and difficult to navigate. Because of -this, a tool called the [AssetBundle Analyzer](https://github.com/faelenor/asset-bundle-analyzer) -was created to make it easier to extract useful information from these files in the form of a -SQLite database. The AssetBundle Analyzer has been quite successful but it has several issues. It -is extremely slow as it runs WebExtract and binary2text on all the AssetBundles of a project and -has to parse very large text files. It can also easily fail because the syntax used by binary2text -is not standard and can even be impossible to parse in some occasions. +The UnityDataTool is a command line tool and showcase of the UnityFileSystemApi native dynamic library. +The main purpose is for analysis of the content of Unity data files, for example AssetBundles and +Player content. + +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. -The UnityFileSystemApi library has been created to expose WebExtract and binary2text -functionalities. This enables the creation of tools that can read Unity data files with TypeTrees. -With it, it becomes very easy to create a binary2text-like tool that can output the data in any -format or a new faster and simpler AssetBundle Analyzer. +It is designed to scale for large build outputs and has been used to fine-tune big Unity-based games. + +The tool also provides comprehensive analysis of **Unity Addressables build reports**, automatically detecting and parsing Addressables JSON build outputs to extract detailed information about bundles, assets, dependencies, file sizes, and build performance metrics. See the [Addressables Build Report Analysis documentation](./Documentation/addressables-build-reports.md) for complete details. + +The command line tool uses the UnityFileSystemApi library to access the content of Unity Archives and Serialized files, which are Unity's primary binary formats. This repository also serves as a reference for how this library could be used as part of incorporating functionality into your own tools. ## Repository content The repository contains the following items: -* UnityFileSystem: source code of a .NET class library exposing the functionalities or the +* [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](Documentation/textdumper.md): a class library that can be used to dump SerializedFiles into + a human-readable format (similar to binary2text). +* [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. * UnityFileSystem.Tests: test suite for the UnityFileSystem library. * UnityFileSystemTestData: the Unity project used to generate the test data. * TestCommon: a helper library used by the test projects. -* [UnityDataTool](UnityDataTool/README.md): a command-line tool providing several features that can - be used to analyze the content of Unity data files. -* [Analyzer](Analyzer/README.md): a class library that can be used to extract key information - from Unity data files and output it into a SQLite database (similar to the - [AssetBundle Analyzer](https://github.com/faelenor/asset-bundle-analyzer)). -* [TextDumper](TextDumper/README.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 - reference chains from objects to other objects using a database created by the Analyzer -## How to build +## Downloads -The projects in this solution require the .NET 6.0 SDK. You can use your favorite IDE to build them. -They were tested in Visual Studio on Windows and Rider on Mac. +Prebuilt Windows and Mac builds are available in the "Actions" tab. Each update to the main branch triggers a new build. -It is also possible to build the projects from the CLI using this command: +To use: +1. Download and unzip the build for your platform. +2. Run UnityDataTool from the extracted location, or add it to your system PATH. -`dotnet build -c Release` +Refer to the [commit history](https://github.com/Unity-Technologies/UnityDataTools/commits/main/) to see the recent improvements to the tool. -## Disclaimer +## Getting UnityFileSystemApi + +UnityFileSystemApi is distributed in the Tools folder of the Unity Editor (from version 2022.1.0a14). The UnityDataTools repository includes a Windows, Mac, and Linux copy of the library in the `UnityFileSystem/` directory. + +The library is backward compatible and can read data files from most Unity versions, so typically the version that is provided with UnityDataTools can be used "as is". + +To analyze data using the library from a specific version of the Unity Editor, copy the appropriate UnityFileSystemApi file from your Unity Editor installation (`{UnityEditor}/Data/Tools/`) to `UnityDataTool/UnityFileSystem/` prior to building: + +The file name is as follows: + +- Windows: `UnityFileSystemApi.dll` +- Mac: `UnityFileSystemApi.dylib` +- Linux: `UnityFileSystemApi.so` -This project is provided on an "as-is" basis and is not officially supported by Unity. It is an -experimental tool provided as an example of what can be done using the UnityFileSystemApi. You can -report bugs and submit pull requests, but there is no guarantee that they will be addressed. +## How to Build + +1. Clone or download this repository. +2. Install the [.NET 9.0 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/9.0). +3. (Optional) Overwrite the checked in version of the UnityFileSystemApi library with one from your Unity Editor installation, as described above. +4. Build using `dotnet build -c Release` or your preferred IDE (tested with Visual Studio and Rider on Windows/Mac). + +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](./Documentation/unitydatatool.md) for usage instructions. + +## Purpose of UnityFileSystemApi + +UnityFileSystemApi exposes the core functionality of WebExtract and binary2text to open and read the Unity Archive and Serialized File formats, exposed as a flexible, performant library. It enables custom tools for binary2text-like output and efficient SQLite database generation. + +## Origins + +This tool is the evolution of the [AssetBundle Analyzer](https://github.com/faelenor/asset-bundle-analyzer) +written by [Francis Pagé](https://www.github.com/faelenor). + +That project was the first to introduce the SQLite database analysis of Unity build output to address +the difficulty of diagnosing build issues through the raw binary2text output, which is large and difficult to navigate. + +The AssetBundle Analyzer was quite successful, but it has several issues. It +is extremely slow as it runs WebExtract and binary2text on all the AssetBundles of a project and +has to parse very large text files. It can also easily fail because the syntax used by binary2text +is not standard and can even be impossible to parse in some occasions. + +UnityDataTools and UnityFileSystemApi were created to overcome these issues, providing fast, reliable access to Unity data files and enabling advanced analysis. + +## Disclaimer ---- -*Footnotes*: 1: AssetBundles include the TypeTree by default but this can - be disabled by using the - [DisableWriteTypeTree](https://docs.unity3d.com/ScriptReference/BuildAssetBundleOptions.DisableWriteTypeTree.html) - option. +This project is provided on an "as-is" basis and is not officially supported by Unity. It is an experimental tool and example of UnityFileSystemApi usage. Bug reports and pull requests are welcome, but support is not guaranteed. 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/ReferenceFinder/ReferenceFinder.csproj b/ReferenceFinder/ReferenceFinder.csproj index 99a08e3..1762447 100644 --- a/ReferenceFinder/ReferenceFinder.csproj +++ b/ReferenceFinder/ReferenceFinder.csproj @@ -2,13 +2,22 @@ Library - net6.0 + net9.0 + latest + + + + AnyCPU + + + + AnyCPU - + diff --git a/ReferenceFinder/ReferenceFinderTool.cs b/ReferenceFinder/ReferenceFinderTool.cs index 4f7bbb4..3c5926a 100644 --- a/ReferenceFinder/ReferenceFinderTool.cs +++ b/ReferenceFinder/ReferenceFinderTool.cs @@ -1,8 +1,8 @@ -using System; +using System; using System.Collections.Generic; using System.Data; -using System.Data.SQLite; using System.IO; +using Microsoft.Data.Sqlite; namespace UnityDataTools.ReferenceFinder; @@ -12,15 +12,15 @@ public ReferenceTreeNode(long id) { Id = id; } - + public readonly long Id; public Dictionary<(long id, string propertyPath), ReferenceTreeNode> Children = new Dictionary<(long, string), ReferenceTreeNode>(); } public class ReferenceFinderTool { - SQLiteCommand m_GetRefsCommand; - SQLiteCommand m_GetObjectCommand; + SqliteCommand m_GetRefsCommand; + SqliteCommand m_GetObjectCommand; List m_Roots = new List(); HashSet<(long, string)> m_ProcessedObjects = new HashSet<(long, string)>(); @@ -29,11 +29,11 @@ public class ReferenceFinderTool public int FindReferences(string objectName, string objectType, string databasePath, string outputFile, bool findAll) { var objectIds = new List(); - SQLiteConnection db; + SqliteConnection db; try { - db = new SQLiteConnection($"Data Source={databasePath};Version=3;Foreign Keys=False;"); + db = new SqliteConnection($"Data Source={databasePath};Version=3;Foreign Keys=False;"); db.Open(); } catch (Exception e) @@ -51,7 +51,7 @@ public int FindReferences(string objectName, string objectType, string databaseP return 1; } - SQLiteCommand getObjectIds; + SqliteCommand getObjectIds; if (objectType != null && objectType != "") { @@ -87,11 +87,11 @@ public int FindReferences(string objectName, string objectType, string databaseP public int FindReferences(long objectId, string databasePath, string outputFile, bool findAll) { var objectIds = new List(); - SQLiteConnection db; + SqliteConnection db; try { - db = new SQLiteConnection($"Data Source={databasePath};Version=3;Foreign Keys=False;"); + db = new SqliteConnection($"Data Source={databasePath};Version=3;Foreign Keys=False;"); db.Open(); } catch (Exception e) @@ -105,13 +105,13 @@ public int FindReferences(long objectId, string databasePath, string outputFile, return FindReferences(db, outputFile, objectIds, findAll); } - int FindReferences(SQLiteConnection db, string outputFile, IList objectIds, bool findAll) + int FindReferences(SqliteConnection db, string outputFile, IList objectIds, bool findAll) { m_Writer = new StreamWriter(outputFile); m_GetRefsCommand = db.CreateCommand(); m_GetRefsCommand.CommandText = @"SELECT object, property_path, EXISTS (SELECT * FROM assets a WHERE a.object = r.object) FROM refs r WHERE referenced_object = @id"; - m_GetRefsCommand.Parameters.Add("@id", DbType.Int64); + m_GetRefsCommand.Parameters.Add("@id", SqliteType.Integer); m_GetObjectCommand = db.CreateCommand(); m_GetObjectCommand.CommandText = @@ -129,7 +129,7 @@ LEFT JOIN refs r '') script FROM object_view o WHERE o.id = @id"; - m_GetObjectCommand.Parameters.Add("@id", DbType.Int64); + m_GetObjectCommand.Parameters.Add("@id", SqliteType.Integer); for (int i = 0; i < objectIds.Count; ++i) { @@ -267,4 +267,4 @@ ReferenceTreeNode ProcessReferences(long id, bool findAll) return wasUsed ? node : null; } -} \ No newline at end of file +} diff --git a/Scripts/comparebuilds.ps1 b/Scripts/comparebuilds.ps1 new file mode 100644 index 0000000..789830b --- /dev/null +++ b/Scripts/comparebuilds.ps1 @@ -0,0 +1,101 @@ +# Example Power Shell script that compare two builds (at the object level). +# It requires that you first run "UnityDataTool analyze" on each build, then pass the resulting databases to this script. +# It requires that sqlite3 is installed, in a location that is available in the PATH environmental variable. + +# Note: This script is intentionally verbose for the sake of demonstration. For very large builds you probably +# would want to hide unchanged objects, which can be achieved with a small change in the embedded SQL statement (see comparebundles.ps1). + +# DISCLAIMER: +# This script is provided "as-is," without any warranty of any kind, express or implied. +# By using this script, you agree that you understand its purpose and that you use it entirely at your own risk. +# The author assumes no liability for any damages resulting from its use, misuse, or inability to use. +# +# Always review and test this script in a safe environment before applying it to a production system. + +param ( + [Parameter(Mandatory=$true, HelpMessage="Path to the first UnityDataTool database")] + [string]$db1, + + [Parameter(Mandatory=$true, HelpMessage="Path to the second UnityDataTool database")] + [string]$db2 +) + +# Check if the database file exists +if (-not (Test-Path $db1)) { + Write-Error "Database file '$db1' not found." + exit 1 +} + +if (-not (Test-Path $db2)) { + Write-Error "Database file '$db2' not found." + exit 1 +} + +# SQL query to compare the content of two builds. +# Note: when the ID of an object changes then it will not be matched as the same. +# Note: matching is done based on the SerializedFile, but not the AssetBundle name. In this way AssetBundles that include +# the content hash in the name can still be compared. +$query = @" +ATTACH DATABASE '$db2' AS db2; + +SELECT + COALESCE(o1.asset_bundle, o2.asset_bundle) AS asset_bundle, + COALESCE(o1.object_id, o2.object_id) AS object_id, + COALESCE(o1.type, o2.type) AS type, + COALESCE(o1.name, o2.name) AS name, + CASE + WHEN o1.asset_bundle IS NULL THEN 'Only in Build 1' + WHEN o2.asset_bundle IS NULL THEN 'Only in Build 2' + WHEN o1.crc32 != o2.crc32 OR o1.size != o2.size THEN 'Different' + ELSE 'Same' + END AS status, + o1.size AS size_build1, + o2.size AS size_build2, + o1.crc32 AS crc32_build1, + o2.crc32 AS crc32_build2 +FROM ( + SELECT + ab.name AS asset_bundle, + o.object_id, + t.name AS type, + o.name, + o.size, + o.crc32, + sf.name AS serialized_file + FROM + objects o + INNER JOIN + types t ON o.type = t.id + INNER JOIN + serialized_files sf ON o.serialized_file = sf.id + LEFT JOIN + asset_bundles ab ON sf.asset_bundle = ab.id +) AS o1 +FULL OUTER JOIN ( + SELECT + ab.name AS asset_bundle, + o.object_id, + t.name AS type, + o.name, + o.size, + o.crc32, + sf.name AS serialized_file + FROM + db2.objects o + INNER JOIN + db2.types t ON o.type = t.id + INNER JOIN + db2.serialized_files sf ON o.serialized_file = sf.id + LEFT JOIN + db2.asset_bundles ab ON sf.asset_bundle = ab.id +) AS o2 ON o1.object_id = o2.object_id + AND o1.type = o2.type + AND o1.name = o2.name + AND o1.serialized_file = o2.serialized_file; + +DETACH DATABASE db2; +"@ + +# Execute the query +$results = sqlite3 $db1 ".mode column" $query +$results | ForEach-Object { Write-Output $_ } diff --git a/Scripts/comparebundles.ps1 b/Scripts/comparebundles.ps1 new file mode 100644 index 0000000..ca18343 --- /dev/null +++ b/Scripts/comparebundles.ps1 @@ -0,0 +1,120 @@ +# PowerShell Script to compare the objects inside two versions of an AssetBundle +# It requires that sqlite3 is installed, in a location that is available in the PATH environmental variable. + +# For the sake of brevity, the query does not list objects when they are the same in both AssetBundles. + +# DISCLAIMER: +# This script is provided "as-is," without any warranty of any kind, express or implied. +# By using this script, you agree that you understand its purpose and that you use it entirely at your own risk. +# The author assumes no liability for any damages resulting from its use, misuse, or inability to use. +# +# Always review and test this script in a safe environment before applying it to a production system. + + +param( + [Parameter(Mandatory=$true, HelpMessage="Path to the first AssetBundle")] + [string]$FileName1, + + [Parameter(Mandatory=$true, HelpMessage="Path to the second AssetBundle")] + [string]$FileName2 +) + +if (-not (Test-Path -Path $FileName1)) { + Write-Error "File '$FileName1' does not exist." + exit 1 +} + +if (-not (Test-Path -Path $FileName2)) { + Write-Error "File '$FileName2' does not exist." + exit 1 +} + +# Separate the directory and file name +$FileDir1 = Split-Path -Path $FileName1 -Parent +$FileLeaf1 = Split-Path -Path $FileName1 -Leaf + +# If no directory is detected (relative file name), use the current working directory +if (-not $FileDir1) { + $FileDir1 = "." +} + +$FileDir2 = Split-Path -Path $FileName2 -Parent +$FileLeaf2 = Split-Path -Path $FileName2 -Leaf +if (-not $FileDir2) { + $FileDir2 = "." +} + +# Retrieve the system's temp folder and create the temporary database file names +$tempFolder = $env:TEMP +$dbName1 = Join-Path -Path $tempFolder -ChildPath ("1_$FileLeaf1.db") +$dbName2 = Join-Path -Path $tempFolder -ChildPath ("2_$FileLeaf2.db") + +#Analyze each AssetBundle into temporary databases +UnityDataTool analyze $FileDir1 -o $dbName1 -p $FileLeaf1 +UnityDataTool analyze $FileDir2 -o $dbName2 -p $FileLeaf2 + +$query = @" +ATTACH DATABASE '$dbName2' AS db2; + +SELECT + COALESCE(o1.serialized_file, o2.serialized_file) AS serialized_file, + COALESCE(o1.object_id, o2.object_id) AS object_id, + COALESCE(o1.type, o2.type) AS type, + COALESCE(o1.name, o2.name) AS name, + CASE + WHEN o1.asset_bundle IS NULL THEN 'Only in Build 1' + WHEN o2.asset_bundle IS NULL THEN 'Only in Build 2' + ELSE 'Different' + END AS status, + o1.size AS size_build1, + o2.size AS size_build2, + o1.crc32 AS crc32_build1, + o2.crc32 AS crc32_build2 +FROM ( + SELECT + ab.name AS asset_bundle, + o.object_id, + t.name AS type, + o.name, + o.size, + o.crc32, + sf.name AS serialized_file + FROM + objects o + INNER JOIN + types t ON o.type = t.id + INNER JOIN + serialized_files sf ON o.serialized_file = sf.id + LEFT JOIN + asset_bundles ab ON sf.asset_bundle = ab.id +) AS o1 +FULL OUTER JOIN ( + SELECT + ab.name AS asset_bundle, + o.object_id, + t.name AS type, + o.name, + o.size, + o.crc32, + sf.name AS serialized_file + FROM + db2.objects o + INNER JOIN + db2.types t ON o.type = t.id + INNER JOIN + db2.serialized_files sf ON o.serialized_file = sf.id + LEFT JOIN + db2.asset_bundles ab ON sf.asset_bundle = ab.id +) AS o2 ON o1.serialized_file = o2.serialized_file + AND o1.object_id = o2.object_id +WHERE NOT (o1.asset_bundle IS NOT NULL AND o2.asset_bundle IS NOT NULL AND o1.crc32 = o2.crc32 AND o1.size = o2.size); + +DETACH DATABASE db2; +"@ + +# Query the database using sqlite3 +sqlite3 $dbName1 ".mode column" $query + +# Delete the temporary database files +Remove-Item $dbName1 -Force +Remove-Item $dbName2 -Force diff --git a/TestCommon/Context.cs b/TestCommon/Context.cs index 2f40652..80601ca 100644 --- a/TestCommon/Context.cs +++ b/TestCommon/Context.cs @@ -1,4 +1,5 @@ -using NUnit.Framework; +using System.IO; +using NUnit.Framework; namespace UnityDataTools.TestCommon; @@ -6,27 +7,27 @@ public class Context { // e.g. /bin/Debug/net6.0/Data/AssetBundles/2021.1.0f1 public string UnityDataFolder { get; } - + // e.g. 2021.1.0f1 public string UnityDataVersion { get; } - + // e.g. /bin/Debug/net6.0/Data public string TestDataFolder { get; } - + // e.g. /bin/Debug/net6.0/ExpectedData/2021.1.0f1 public string ExpectedDataFolder { get; } - + public ExpectedData ExpectedData { get; } = new(); public Context(string folder) { var di = new DirectoryInfo(folder); - + UnityDataFolder = folder; UnityDataVersion = di.Name; TestDataFolder = di.Parent.Parent.FullName; ExpectedDataFolder = Path.Combine(di.Parent.Parent.Parent.FullName, "ExpectedData", UnityDataVersion); - + ExpectedData.Load(ExpectedDataFolder); } diff --git a/TestCommon/Data/AddressableBuildLayouts/buildlayout_2025.01.28.16.35.01.json b/TestCommon/Data/AddressableBuildLayouts/buildlayout_2025.01.28.16.35.01.json new file mode 100644 index 0000000..897eae6 --- /dev/null +++ b/TestCommon/Data/AddressableBuildLayouts/buildlayout_2025.01.28.16.35.01.json @@ -0,0 +1,2 @@ +{"BuildTarget":19,"BuildResultHash":"a762437258c504cc7f18518dc9663c2b","BuildType":0,"BuildStartTime":"1/28/2025 4:35:01 PM","Duration":2.5228200999999998,"BuildError":"", +"UnityVersion":"6000.1.0b2","PackageVersion":"com.unity.addressables: 2.2.2","PlayerBuildVersion":"1.0","AddressablesEditorSettings":{"SettingsHash":"1595be5d5bf5de0ac1319ff503981501","ActiveProfile":{"Name":"Default","Id":"eb1018326f70bd440b0fda653327eb37","Values":[{"Key":"027d618fc8b846c43bde4f69b94d6548","Value":"ServerData/[BuildTarget]"},{"Key":"091eb06bd685ad44b84643456262ddb9","Value":""},{"Key":"69a2bcc4bc4078d4d81a27ccb79d293d","Value":"[UnityEngine.AddressableAssets.Addressables.BuildPath]/[BuildTarget]"},{"Key":"8867b9b32191a4a48bc3deccf931a225","Value":"[UnityEditor.EditorUserBuildSettings.activeBuildTarget]"},{"Key":"db7c73bb3ea77cd468fe4a345d8f45be","Value":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}/[BuildTarget]"}]},"BuildRemoteCatalog":false,"RemoteCatalogLoadPath":"","BundleLocalCatalog":false,"OptimizeCatalogSize":false,"CatalogRequestsTimeout":0,"MaxConcurrentWebRequests":3,"DisableCatalogUpdateOnStartup":false,"UniqueBundleIds":false,"EnableJsonCatalog":false,"NonRecursiveBuilding":true,"ContiguousBundles":true,"DisableSubAssetRepresentations":false,"ShaderBundleNaming":"ProjectName","MonoScriptBundleNaming":"ProjectName","StripUnityVersionFromBundleBuild":false},"AddressablesRuntimeSettings":{"LogResourceManagerExceptions":true,"CatalogLoadPaths":["{UnityEngine.AddressableAssets.Addressables.RuntimePath}/catalog.bin"],"CatalogHash":"e3c139a75c54ca9554a77ea3bb354cb1"},"BuildScript":"Default Build Script","DefaultGroup":{"rid":1000},"Groups":[{"rid":1001},{"rid":1000},{"rid":1002},{"rid":1003}],"BuiltInBundles":[],"DuplicatedAssets":[],"LocalCatalogBuildPath":"Library/com.unity.addressables/aa/Windows/StandaloneWindows64","RemoteCatalogBuildPath":"","references":{"version":2,"RefIds":[{"rid":1000,"type":{"class":"BuildLayout/Group","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"Default Local Group","Guid":"336a756d05aafc24295e5bd4d29d32fd","PackingMode":"PackTogether","Bundles":[],"Schemas":[{"rid":1004},{"rid":1005}]}},{"rid":1001,"type":{"class":"BuildLayout/Group","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"SamplePack1","Guid":"258c7459604d085478c05930706078c2","PackingMode":"PackSeparately","Bundles":[{"rid":1006},{"rid":1007},{"rid":1008},{"rid":1009},{"rid":1010},{"rid":1011},{"rid":1012},{"rid":1013},{"rid":1014},{"rid":1015}],"Schemas":[{"rid":1016},{"rid":1017}]}},{"rid":1002,"type":{"class":"BuildLayout/Group","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"Generated","Guid":"a201e34a5a3fa024080d99bcfceca817","PackingMode":"PackSeparately","Bundles":[{"rid":1018},{"rid":1019},{"rid":1020},{"rid":1021},{"rid":1022},{"rid":1023},{"rid":1024}],"Schemas":[{"rid":1025},{"rid":1026}]}},{"rid":1003,"type":{"class":"BuildLayout/Group","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"SamplePack2","Guid":"a56c5fee201ac7840a927f1c63a68728","PackingMode":"PackSeparately","Bundles":[{"rid":1027},{"rid":1028},{"rid":1029},{"rid":1030},{"rid":1031},{"rid":1032},{"rid":1033},{"rid":1034},{"rid":1035},{"rid":1036}],"Schemas":[{"rid":1037},{"rid":1038}]}},{"rid":1004,"type":{"class":"BuildLayout/SchemaData","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"59cf7bcd70ab1b04c9384d55c7283117","Type":"BundledAssetGroupSchema","SchemaDataPairs":[{"Key":"InternalBundleIdMode","Value":"GroupGuidProjectIdHash"},{"Key":"Compression","Value":"LZ4"},{"Key":"IncludeAddressInCatalog","Value":"True"},{"Key":"IncludeGUIDInCatalog","Value":"True"},{"Key":"IncludeLabelsInCatalog","Value":"True"},{"Key":"InternalIdNamingMode","Value":"FullPath"},{"Key":"AssetBundledCacheClearBehavior","Value":"ClearWhenSpaceIsNeededInCache"},{"Key":"IncludeInBuild","Value":"True"},{"Key":"BundledAssetProviderType","Value":"UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider"},{"Key":"ForceUniqueProvider","Value":"False"},{"Key":"UseAssetBundleCache","Value":"True"},{"Key":"UseAssetBundleCrc","Value":"True"},{"Key":"UseAssetBundleCrcForCachedBundles","Value":"True"},{"Key":"UseUnityWebRequestForLocalBundles","Value":"False"},{"Key":"Timeout","Value":"0"},{"Key":"ChunkedTransfer","Value":"False"},{"Key":"RedirectLimit","Value":"-1"},{"Key":"RetryCount","Value":"0"},{"Key":"BuildPath","Value":"Library/com.unity.addressables/aa/Windows/StandaloneWindows64"},{"Key":"LoadPath","Value":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}/StandaloneWindows64"},{"Key":"PackingMode","Value":"PackTogether"},{"Key":"AssetBundleProviderType","Value":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider"},{"Key":"UseDefaultSchemaSettings","Value":"False"},{"Key":"SelectedPathPairIndex","Value":"0"},{"Key":"BundleNaming","Value":"AppendHash"},{"Key":"AssetLoadMode","Value":"RequestedAssetAndDependencies"}]}},{"rid":1005,"type":{"class":"BuildLayout/SchemaData","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"b819cb823fb33024bbd3ac2f201858d8","Type":"ContentUpdateGroupSchema","SchemaDataPairs":[{"Key":"StaticContent","Value":"False"}]}},{"rid":1006,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack1_assets_4.bundle","InternalName":"1f8d4b3797eb32be56cfe202caec6fd6","FileSize":23040,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":3983860733,"Hash":{"serializedVersion":"2","Hash":"8537ba5e4a59fcb3afad3b35878f92b5"},"Group":{"rid":1001},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack1_assets_4.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1039}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1007,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack1_assets_6.bundle","InternalName":"3b83c8d34ca68f6e8852ceeb09c1eac4","FileSize":34528,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":4276430644,"Hash":{"serializedVersion":"2","Hash":"ac97b5cd2d8c21ca5ce5b9f37d83bdd8"},"Group":{"rid":1001},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack1_assets_6.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1040}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1008,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack1_assets_3.bundle","InternalName":"1ff936ac860951602e79522deaa8c9c6","FileSize":25920,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":3072052095,"Hash":{"serializedVersion":"2","Hash":"2f713cc6c5ac93944787e53abc171506"},"Group":{"rid":1001},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack1_assets_3.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1041}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1009,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack1_assets_5.bundle","InternalName":"69cc943b938e112fdb596763b7e3325c","FileSize":22336,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":1442816670,"Hash":{"serializedVersion":"2","Hash":"deaa6a181faf7460597c59a1ccca07ab"},"Group":{"rid":1001},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack1_assets_5.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1042}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1010,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack1_assets_2.bundle","InternalName":"6b51b282e9b4dd13e69d0739a380a55b","FileSize":22624,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":63243828,"Hash":{"serializedVersion":"2","Hash":"3a6d4700474b3c0f8d50dfe7cff32fef"},"Group":{"rid":1001},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack1_assets_2.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1043}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1011,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack1_assets_7.bundle","InternalName":"175e752b232a665bd379dadef49c596c","FileSize":18816,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":1064022522,"Hash":{"serializedVersion":"2","Hash":"1b40b4f0c23bf37432b5adcefce3bfe6"},"Group":{"rid":1001},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack1_assets_7.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1044}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1012,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack1_assets_0.bundle","InternalName":"9b397865eb247e7fd24573450180e68a","FileSize":33824,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":1395461927,"Hash":{"serializedVersion":"2","Hash":"1643a503052be3f585a8f1813952aa2e"},"Group":{"rid":1001},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack1_assets_0.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1045}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1013,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack1_assets_8.bundle","InternalName":"81bd30248e70a514207fe576ea8be326","FileSize":15264,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":1715313454,"Hash":{"serializedVersion":"2","Hash":"4d35b226b8f9624a4105997b26d8aa7d"},"Group":{"rid":1001},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack1_assets_8.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1046}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1014,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack1_assets_1.bundle","InternalName":"6842397c643be2ce3b57ace165f4865d","FileSize":21440,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":1296609683,"Hash":{"serializedVersion":"2","Hash":"ab22469cbd3dbdfdc5aec3d50fc1ea9a"},"Group":{"rid":1001},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack1_assets_1.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1047}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1015,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack1_assets_9.bundle","InternalName":"bf7bc97bd4ced20925b9bd0e81f2d522","FileSize":19008,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":2442388881,"Hash":{"serializedVersion":"2","Hash":"1b7797b09d636b30e451e3509f17085f"},"Group":{"rid":1001},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack1_assets_9.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1048}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1016,"type":{"class":"BuildLayout/SchemaData","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"c57adc9566661284e96157fafecd9441","Type":"BundledAssetGroupSchema","SchemaDataPairs":[{"Key":"InternalBundleIdMode","Value":"GroupGuidProjectIdHash"},{"Key":"Compression","Value":"Uncompressed"},{"Key":"IncludeAddressInCatalog","Value":"True"},{"Key":"IncludeGUIDInCatalog","Value":"True"},{"Key":"IncludeLabelsInCatalog","Value":"True"},{"Key":"InternalIdNamingMode","Value":"FullPath"},{"Key":"AssetBundledCacheClearBehavior","Value":"ClearWhenSpaceIsNeededInCache"},{"Key":"IncludeInBuild","Value":"True"},{"Key":"BundledAssetProviderType","Value":"UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider"},{"Key":"ForceUniqueProvider","Value":"False"},{"Key":"UseAssetBundleCache","Value":"True"},{"Key":"UseAssetBundleCrc","Value":"True"},{"Key":"UseAssetBundleCrcForCachedBundles","Value":"True"},{"Key":"UseUnityWebRequestForLocalBundles","Value":"False"},{"Key":"Timeout","Value":"0"},{"Key":"ChunkedTransfer","Value":"False"},{"Key":"RedirectLimit","Value":"-1"},{"Key":"RetryCount","Value":"0"},{"Key":"BuildPath","Value":"Library/com.unity.addressables/aa/Windows/StandaloneWindows64"},{"Key":"LoadPath","Value":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}/StandaloneWindows64"},{"Key":"PackingMode","Value":"PackSeparately"},{"Key":"AssetBundleProviderType","Value":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider"},{"Key":"UseDefaultSchemaSettings","Value":"False"},{"Key":"SelectedPathPairIndex","Value":"0"},{"Key":"BundleNaming","Value":"NoHash"},{"Key":"AssetLoadMode","Value":"RequestedAssetAndDependencies"}]}},{"rid":1017,"type":{"class":"BuildLayout/SchemaData","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"c50c0af57295b8c42bb5d7d31526d100","Type":"ContentUpdateGroupSchema","SchemaDataPairs":[{"Key":"StaticContent","Value":"False"}]}},{"rid":1018,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"generated_assets_drum1.bundle","InternalName":"eb15f892bfc01dd4aa0ece86f7c590cb","FileSize":13872,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":694414153,"Hash":{"serializedVersion":"2","Hash":"fbfc3eaefa459f1a8dc4c47944041e49"},"Group":{"rid":1002},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\generated_assets_drum1.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1049}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1019,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"generated_assets_brownian.bundle","InternalName":"4f7dd1966bff4c80539f0a11bfb2f604","FileSize":589168,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":1844209987,"Hash":{"serializedVersion":"2","Hash":"5fbf9ee0ebb26d30d3cc0d679905be55"},"Group":{"rid":1002},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\generated_assets_brownian.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1050}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1020,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"generated_assets_sine440.bundle","InternalName":"263ef2f877d5a34b7df03dd3b5bbeac4","FileSize":9456,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":1802149312,"Hash":{"serializedVersion":"2","Hash":"3f0088ca1daf7287c0a6d442d28ab16c"},"Group":{"rid":1002},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\generated_assets_sine440.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1051}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1021,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"generated_assets_sine.bundle","InternalName":"b27a60ba4c0ec77b926f8c00dda0f250","FileSize":80368,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":632252596,"Hash":{"serializedVersion":"2","Hash":"b12d2962cea07e4ccad683f1cd676b2f"},"Group":{"rid":1002},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\generated_assets_sine.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1052}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1022,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"generated_assets_square.bundle","InternalName":"0458b4a804a69eb9a2a1833017035327","FileSize":415440,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":3072331629,"Hash":{"serializedVersion":"2","Hash":"107dfced85b01503feaa82ace8c8eb6e"},"Group":{"rid":1002},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\generated_assets_square.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1053}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1023,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"generated_assets_square300.bundle","InternalName":"440d49ce6d183b7c229bfa22d06bb0c9","FileSize":54720,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":2355528378,"Hash":{"serializedVersion":"2","Hash":"ca4dba1b947d28a787c26d35eaaaf1cc"},"Group":{"rid":1002},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\generated_assets_square300.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1054}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1024,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"generated_assets_drum2.bundle","InternalName":"1cbb611c300205dd4ff1d1a4062715cd","FileSize":14032,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":4195441782,"Hash":{"serializedVersion":"2","Hash":"389ddf22f270063e7e15b2854e7e645d"},"Group":{"rid":1002},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\generated_assets_drum2.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1055}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1025,"type":{"class":"BuildLayout/SchemaData","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"4078d0d3d05126f4ca5d1809fb9865b0","Type":"BundledAssetGroupSchema","SchemaDataPairs":[{"Key":"InternalBundleIdMode","Value":"GroupGuidProjectIdHash"},{"Key":"Compression","Value":"Uncompressed"},{"Key":"IncludeAddressInCatalog","Value":"True"},{"Key":"IncludeGUIDInCatalog","Value":"True"},{"Key":"IncludeLabelsInCatalog","Value":"True"},{"Key":"InternalIdNamingMode","Value":"FullPath"},{"Key":"AssetBundledCacheClearBehavior","Value":"ClearWhenSpaceIsNeededInCache"},{"Key":"IncludeInBuild","Value":"True"},{"Key":"BundledAssetProviderType","Value":"UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider"},{"Key":"ForceUniqueProvider","Value":"False"},{"Key":"UseAssetBundleCache","Value":"True"},{"Key":"UseAssetBundleCrc","Value":"True"},{"Key":"UseAssetBundleCrcForCachedBundles","Value":"True"},{"Key":"UseUnityWebRequestForLocalBundles","Value":"False"},{"Key":"Timeout","Value":"0"},{"Key":"ChunkedTransfer","Value":"False"},{"Key":"RedirectLimit","Value":"-1"},{"Key":"RetryCount","Value":"0"},{"Key":"BuildPath","Value":"Library/com.unity.addressables/aa/Windows/StandaloneWindows64"},{"Key":"LoadPath","Value":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}/StandaloneWindows64"},{"Key":"PackingMode","Value":"PackSeparately"},{"Key":"AssetBundleProviderType","Value":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider"},{"Key":"UseDefaultSchemaSettings","Value":"False"},{"Key":"SelectedPathPairIndex","Value":"0"},{"Key":"BundleNaming","Value":"NoHash"},{"Key":"AssetLoadMode","Value":"RequestedAssetAndDependencies"}]}},{"rid":1026,"type":{"class":"BuildLayout/SchemaData","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"7550134532116b546bbfc73d635d238d","Type":"ContentUpdateGroupSchema","SchemaDataPairs":[{"Key":"StaticContent","Value":"False"}]}},{"rid":1027,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack2_assets_e.bundle","InternalName":"6474f377f608a8c16ff891aed86ad9c6","FileSize":73376,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":4278687744,"Hash":{"serializedVersion":"2","Hash":"e73641e44d76286924c42a4869ab1dd1"},"Group":{"rid":1003},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack2_assets_e.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1056}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1028,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack2_assets_c.bundle","InternalName":"3cd71b90318e38bf58581044d4347d69","FileSize":21120,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":1624109646,"Hash":{"serializedVersion":"2","Hash":"31d562de9449e50f1bbc6496696f4ea1"},"Group":{"rid":1003},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack2_assets_c.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1057}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1029,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack2_assets_j.bundle","InternalName":"b64b402edb473d5183e9e428f8a744a2","FileSize":62976,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":3241181028,"Hash":{"serializedVersion":"2","Hash":"93e2176fdb1e52c234611e32440653b6"},"Group":{"rid":1003},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack2_assets_j.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1058}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1030,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack2_assets_h.bundle","InternalName":"3b16fc37c6221a87f82c9aadd34e7af4","FileSize":70976,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":1058037547,"Hash":{"serializedVersion":"2","Hash":"130c21f4f6fab54624f9929178da838d"},"Group":{"rid":1003},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack2_assets_h.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1059}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1031,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack2_assets_f.bundle","InternalName":"5e3b8d40fbcd8f3c3b4e8eb144de6af4","FileSize":54528,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":1251071412,"Hash":{"serializedVersion":"2","Hash":"e02ddc8784b1ead17d021bc3f7704ed3"},"Group":{"rid":1003},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack2_assets_f.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1060}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1032,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack2_assets_b.bundle","InternalName":"cc6a2013cf6341e142c7a2937d22b6b8","FileSize":23488,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":3745088177,"Hash":{"serializedVersion":"2","Hash":"72fd915bc88a0e3122bdf8410d84498b"},"Group":{"rid":1003},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack2_assets_b.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1061}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1033,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack2_assets_d.bundle","InternalName":"b9872cdeb1f364a3531aca1beeae2c77","FileSize":134464,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":650826451,"Hash":{"serializedVersion":"2","Hash":"e2b46d4caabe1bfd5ec92115bff593fa"},"Group":{"rid":1003},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack2_assets_d.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1062}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1034,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack2_assets_a.bundle","InternalName":"30ae8f6391be92f8f247d3c771fb2c1e","FileSize":51264,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":597684922,"Hash":{"serializedVersion":"2","Hash":"4bdff88ddff01b857c9f2584d4c73fbe"},"Group":{"rid":1003},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack2_assets_a.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1063}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1035,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack2_assets_i.bundle","InternalName":"c5ea5ba50ff6c704382d42bff195c10e","FileSize":79456,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":857134522,"Hash":{"serializedVersion":"2","Hash":"d1e23b53f215bf4997133b70f80f3634"},"Group":{"rid":1003},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack2_assets_i.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1064}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1036,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack2_assets_g.bundle","InternalName":"bb3e6d6039b26a029b9c7cb8edaa8e2a","FileSize":65088,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":2918875995,"Hash":{"serializedVersion":"2","Hash":"ee3c751b3c0b5649c00d41d0728752b5"},"Group":{"rid":1003},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack2_assets_g.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1065}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1037,"type":{"class":"BuildLayout/SchemaData","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"a5af9a2aae0a45347a8a5ef8fafece02","Type":"BundledAssetGroupSchema","SchemaDataPairs":[{"Key":"InternalBundleIdMode","Value":"GroupGuidProjectIdHash"},{"Key":"Compression","Value":"Uncompressed"},{"Key":"IncludeAddressInCatalog","Value":"True"},{"Key":"IncludeGUIDInCatalog","Value":"True"},{"Key":"IncludeLabelsInCatalog","Value":"True"},{"Key":"InternalIdNamingMode","Value":"FullPath"},{"Key":"AssetBundledCacheClearBehavior","Value":"ClearWhenSpaceIsNeededInCache"},{"Key":"IncludeInBuild","Value":"True"},{"Key":"BundledAssetProviderType","Value":"UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider"},{"Key":"ForceUniqueProvider","Value":"False"},{"Key":"UseAssetBundleCache","Value":"True"},{"Key":"UseAssetBundleCrc","Value":"True"},{"Key":"UseAssetBundleCrcForCachedBundles","Value":"True"},{"Key":"UseUnityWebRequestForLocalBundles","Value":"False"},{"Key":"Timeout","Value":"0"},{"Key":"ChunkedTransfer","Value":"False"},{"Key":"RedirectLimit","Value":"-1"},{"Key":"RetryCount","Value":"0"},{"Key":"BuildPath","Value":"Library/com.unity.addressables/aa/Windows/StandaloneWindows64"},{"Key":"LoadPath","Value":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}/StandaloneWindows64"},{"Key":"PackingMode","Value":"PackSeparately"},{"Key":"AssetBundleProviderType","Value":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider"},{"Key":"UseDefaultSchemaSettings","Value":"False"},{"Key":"SelectedPathPairIndex","Value":"0"},{"Key":"BundleNaming","Value":"NoHash"},{"Key":"AssetLoadMode","Value":"RequestedAssetAndDependencies"}]}},{"rid":1038,"type":{"class":"BuildLayout/SchemaData","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"c0dbabc9705c9644d9a44d00bd653470","Type":"ContentUpdateGroupSchema","SchemaDataPairs":[{"Key":"StaticContent","Value":"False"}]}},{"rid":1039,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-73b98a43ee020a5e591db9518c2cb296/CAB-73b98a43ee020a5e591db9518c2cb296","Bundle":{"rid":1006},"SubFiles":[{"rid":1066},{"rid":1067}],"Assets":[{"rid":1068}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1040,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-27752acd531d26b983a15c00264c45fb/CAB-27752acd531d26b983a15c00264c45fb","Bundle":{"rid":1007},"SubFiles":[{"rid":1069},{"rid":1070}],"Assets":[{"rid":1071}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1041,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-1f5bcdc7a5409950f693976900dd4aa2/CAB-1f5bcdc7a5409950f693976900dd4aa2","Bundle":{"rid":1008},"SubFiles":[{"rid":1072},{"rid":1073}],"Assets":[{"rid":1074}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1042,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-0fa1a01f0e1b970a85c89e575595f749/CAB-0fa1a01f0e1b970a85c89e575595f749","Bundle":{"rid":1009},"SubFiles":[{"rid":1075},{"rid":1076}],"Assets":[{"rid":1077}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1043,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-01315ab73001d0050a9e9a4454dc947f/CAB-01315ab73001d0050a9e9a4454dc947f","Bundle":{"rid":1010},"SubFiles":[{"rid":1078},{"rid":1079}],"Assets":[{"rid":1080}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1044,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-33b13439bdd10f67ea9ad546608069c7/CAB-33b13439bdd10f67ea9ad546608069c7","Bundle":{"rid":1011},"SubFiles":[{"rid":1081},{"rid":1082}],"Assets":[{"rid":1083}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1045,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-70392fbb89818a9b1dc85933205fc393/CAB-70392fbb89818a9b1dc85933205fc393","Bundle":{"rid":1012},"SubFiles":[{"rid":1084},{"rid":1085}],"Assets":[{"rid":1086}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1046,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-0779c46a926a678e417643586b001645/CAB-0779c46a926a678e417643586b001645","Bundle":{"rid":1013},"SubFiles":[{"rid":1087},{"rid":1088}],"Assets":[{"rid":1089}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1047,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-0a2912740c976ca7a7b1ccc28e6e7a7a/CAB-0a2912740c976ca7a7b1ccc28e6e7a7a","Bundle":{"rid":1014},"SubFiles":[{"rid":1090},{"rid":1091}],"Assets":[{"rid":1092}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1048,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-05d1c88fa8f8866205a8423046199e1e/CAB-05d1c88fa8f8866205a8423046199e1e","Bundle":{"rid":1015},"SubFiles":[{"rid":1093},{"rid":1094}],"Assets":[{"rid":1095}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1049,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-d021e216fd6d23c0d84b2c10d7d2fe9b/CAB-d021e216fd6d23c0d84b2c10d7d2fe9b","Bundle":{"rid":1018},"SubFiles":[{"rid":1096},{"rid":1097}],"Assets":[{"rid":1098}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":212},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1050,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-e02a12723316bd57c4529362df4c1c7b/CAB-e02a12723316bd57c4529362df4c1c7b","Bundle":{"rid":1019},"SubFiles":[{"rid":1099},{"rid":1100}],"Assets":[{"rid":1101}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":216},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1051,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-dfc27c99e8d3e8200d1e73d09ba342fc/CAB-dfc27c99e8d3e8200d1e73d09ba342fc","Bundle":{"rid":1020},"SubFiles":[{"rid":1102},{"rid":1103}],"Assets":[{"rid":1104}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":212},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1052,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-d8c2508dc18ced397b0b7ea3e293046b/CAB-d8c2508dc18ced397b0b7ea3e293046b","Bundle":{"rid":1021},"SubFiles":[{"rid":1105},{"rid":1106}],"Assets":[{"rid":1107}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":212},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1053,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-ab13095ed3bae6812ebbdefdf8c8fb30/CAB-ab13095ed3bae6812ebbdefdf8c8fb30","Bundle":{"rid":1022},"SubFiles":[{"rid":1108},{"rid":1109}],"Assets":[{"rid":1110}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":212},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1054,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-6d55f28146ef170678a8f3a3057434a1/CAB-6d55f28146ef170678a8f3a3057434a1","Bundle":{"rid":1023},"SubFiles":[{"rid":1111},{"rid":1112}],"Assets":[{"rid":1113}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":216},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1055,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-2d0e0b283a1fb1a45b272552509552ea/CAB-2d0e0b283a1fb1a45b272552509552ea","Bundle":{"rid":1024},"SubFiles":[{"rid":1114},{"rid":1115}],"Assets":[{"rid":1116}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":212},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1056,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-2a6b5cdb2a206a3d4aa0eab375f1e5d6/CAB-2a6b5cdb2a206a3d4aa0eab375f1e5d6","Bundle":{"rid":1027},"SubFiles":[{"rid":1117},{"rid":1118}],"Assets":[{"rid":1119}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1057,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-a209ca6fc6e41ac9f6d6fc1d57dc96e3/CAB-a209ca6fc6e41ac9f6d6fc1d57dc96e3","Bundle":{"rid":1028},"SubFiles":[{"rid":1120},{"rid":1121}],"Assets":[{"rid":1122}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1058,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-5319be78ac1d58d02d94d24bec324ca6/CAB-5319be78ac1d58d02d94d24bec324ca6","Bundle":{"rid":1029},"SubFiles":[{"rid":1123},{"rid":1124}],"Assets":[{"rid":1125}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1059,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-8cc6bc79beeec25a3b2e09484debb7bf/CAB-8cc6bc79beeec25a3b2e09484debb7bf","Bundle":{"rid":1030},"SubFiles":[{"rid":1126},{"rid":1127}],"Assets":[{"rid":1128}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1060,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-e74367794c024c21ece7e1aa44e43744/CAB-e74367794c024c21ece7e1aa44e43744","Bundle":{"rid":1031},"SubFiles":[{"rid":1129},{"rid":1130}],"Assets":[{"rid":1131}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1061,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-84aa7474c89cce2aa8ded8d7683cb357/CAB-84aa7474c89cce2aa8ded8d7683cb357","Bundle":{"rid":1032},"SubFiles":[{"rid":1132},{"rid":1133}],"Assets":[{"rid":1134}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1062,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-a2439cd83acd1c60c58561542cf99432/CAB-a2439cd83acd1c60c58561542cf99432","Bundle":{"rid":1033},"SubFiles":[{"rid":1135},{"rid":1136}],"Assets":[{"rid":1137}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1063,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-fbb9c1b8073b821e042a5bc47ba09cf7/CAB-fbb9c1b8073b821e042a5bc47ba09cf7","Bundle":{"rid":1034},"SubFiles":[{"rid":1138},{"rid":1139}],"Assets":[{"rid":1140}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1064,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-f6c891248d11a37872b027b369b820af/CAB-f6c891248d11a37872b027b369b820af","Bundle":{"rid":1035},"SubFiles":[{"rid":1141},{"rid":1142}],"Assets":[{"rid":1143}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1065,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-2c2d6e9b78804ee1e800e2b1c620f98a/CAB-2c2d6e9b78804ee1e800e2b1c620f98a","Bundle":{"rid":1036},"SubFiles":[{"rid":1144},{"rid":1145}],"Assets":[{"rid":1146}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1066,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-73b98a43ee020a5e591db9518c2cb296","IsSerializedFile":true,"Size":3680}},{"rid":1067,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-73b98a43ee020a5e591db9518c2cb296.resource","IsSerializedFile":false,"Size":19200}},{"rid":1068,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"054a1ca02d98d394b85418dd05dad9ff","AssetPath":"Assets/Samples/Saxophone1/4.mp3","InternalId":"Assets/Samples/Saxophone1/4.mp3","AssetHash":{"serializedVersion":"2","Hash":"5583cbfc81d477a4697fafa00f34c984"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"4","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":19200,"References":[]}],"MainAssetType":6,"GroupGuid":"258c7459604d085478c05930706078c2","AddressableName":"4","Labels":[],"SerializedSize":156,"StreamedSize":19200,"File":{"rid":1039},"Bundle":{"rid":1006},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1069,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-27752acd531d26b983a15c00264c45fb","IsSerializedFile":true,"Size":3680}},{"rid":1070,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-27752acd531d26b983a15c00264c45fb.resource","IsSerializedFile":false,"Size":30688}},{"rid":1071,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"278c261333bf8604eb5c83790d02004d","AssetPath":"Assets/Samples/Saxophone1/6.mp3","InternalId":"Assets/Samples/Saxophone1/6.mp3","AssetHash":{"serializedVersion":"2","Hash":"4b21a94723f1b72670836b8034ae42a0"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"6","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":30688,"References":[]}],"MainAssetType":6,"GroupGuid":"258c7459604d085478c05930706078c2","AddressableName":"6","Labels":[],"SerializedSize":156,"StreamedSize":30688,"File":{"rid":1040},"Bundle":{"rid":1007},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1072,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-1f5bcdc7a5409950f693976900dd4aa2","IsSerializedFile":true,"Size":3676}},{"rid":1073,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-1f5bcdc7a5409950f693976900dd4aa2.resource","IsSerializedFile":false,"Size":22080}},{"rid":1074,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"43b17a5450ece6b43a6b459b5eeaf648","AssetPath":"Assets/Samples/Saxophone1/3.mp3","InternalId":"Assets/Samples/Saxophone1/3.mp3","AssetHash":{"serializedVersion":"2","Hash":"fd3c09c06b08e3de6829d657a581f45c"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"3","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":22080,"References":[]}],"MainAssetType":6,"GroupGuid":"258c7459604d085478c05930706078c2","AddressableName":"3","Labels":[],"SerializedSize":156,"StreamedSize":22080,"File":{"rid":1041},"Bundle":{"rid":1008},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1075,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-0fa1a01f0e1b970a85c89e575595f749","IsSerializedFile":true,"Size":3676}},{"rid":1076,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-0fa1a01f0e1b970a85c89e575595f749.resource","IsSerializedFile":false,"Size":18496}},{"rid":1077,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"870edf9b56842b44f8f5e92f0ea5a76e","AssetPath":"Assets/Samples/Saxophone1/5.mp3","InternalId":"Assets/Samples/Saxophone1/5.mp3","AssetHash":{"serializedVersion":"2","Hash":"cf947973463a1e4e2df69fc10197fb4d"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"5","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":18496,"References":[]}],"MainAssetType":6,"GroupGuid":"258c7459604d085478c05930706078c2","AddressableName":"5","Labels":[],"SerializedSize":156,"StreamedSize":18496,"File":{"rid":1042},"Bundle":{"rid":1009},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1078,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-01315ab73001d0050a9e9a4454dc947f","IsSerializedFile":true,"Size":3680}},{"rid":1079,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-01315ab73001d0050a9e9a4454dc947f.resource","IsSerializedFile":false,"Size":18784}},{"rid":1080,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"9607360293b294646ac2684e8de61c4e","AssetPath":"Assets/Samples/Saxophone1/2.mp3","InternalId":"Assets/Samples/Saxophone1/2.mp3","AssetHash":{"serializedVersion":"2","Hash":"0b88ccd96e292fdb6f5b286deb1a29e9"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"2","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":18784,"References":[]}],"MainAssetType":6,"GroupGuid":"258c7459604d085478c05930706078c2","AddressableName":"2","Labels":[],"SerializedSize":156,"StreamedSize":18784,"File":{"rid":1043},"Bundle":{"rid":1010},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1081,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-33b13439bdd10f67ea9ad546608069c7","IsSerializedFile":true,"Size":3676}},{"rid":1082,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-33b13439bdd10f67ea9ad546608069c7.resource","IsSerializedFile":false,"Size":14976}},{"rid":1083,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"aedaaf69c851e3d47bff4d6408971c31","AssetPath":"Assets/Samples/Saxophone1/7.mp3","InternalId":"Assets/Samples/Saxophone1/7.mp3","AssetHash":{"serializedVersion":"2","Hash":"308e89a7fc814e86b6a4ddeffb537aea"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"7","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":14976,"References":[]}],"MainAssetType":6,"GroupGuid":"258c7459604d085478c05930706078c2","AddressableName":"7","Labels":[],"SerializedSize":156,"StreamedSize":14976,"File":{"rid":1044},"Bundle":{"rid":1011},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1084,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-70392fbb89818a9b1dc85933205fc393","IsSerializedFile":true,"Size":3676}},{"rid":1085,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-70392fbb89818a9b1dc85933205fc393.resource","IsSerializedFile":false,"Size":29984}},{"rid":1086,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"bb0e3bc1035198b4ba3589f09da21214","AssetPath":"Assets/Samples/Saxophone1/0.mp3","InternalId":"Assets/Samples/Saxophone1/0.mp3","AssetHash":{"serializedVersion":"2","Hash":"4005ae0b3feb4602beb3076b410a4f0d"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"0","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":29984,"References":[]}],"MainAssetType":6,"GroupGuid":"258c7459604d085478c05930706078c2","AddressableName":"0","Labels":[],"SerializedSize":156,"StreamedSize":29984,"File":{"rid":1045},"Bundle":{"rid":1012},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1087,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-0779c46a926a678e417643586b001645","IsSerializedFile":true,"Size":3680}},{"rid":1088,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-0779c46a926a678e417643586b001645.resource","IsSerializedFile":false,"Size":11424}},{"rid":1089,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"c09ca9d2c17ea134eac56c494c04a1b8","AssetPath":"Assets/Samples/Saxophone1/8.mp3","InternalId":"Assets/Samples/Saxophone1/8.mp3","AssetHash":{"serializedVersion":"2","Hash":"6c77f2c27e4bee363ba77476f17f5f14"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"8","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":11424,"References":[]}],"MainAssetType":6,"GroupGuid":"258c7459604d085478c05930706078c2","AddressableName":"8","Labels":[],"SerializedSize":156,"StreamedSize":11424,"File":{"rid":1046},"Bundle":{"rid":1013},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1090,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-0a2912740c976ca7a7b1ccc28e6e7a7a","IsSerializedFile":true,"Size":3680}},{"rid":1091,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-0a2912740c976ca7a7b1ccc28e6e7a7a.resource","IsSerializedFile":false,"Size":17600}},{"rid":1092,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"d0c2f86c1157074499c565080193d5a5","AssetPath":"Assets/Samples/Saxophone1/1.mp3","InternalId":"Assets/Samples/Saxophone1/1.mp3","AssetHash":{"serializedVersion":"2","Hash":"fa8a03b7ce0e720d3fafdae3bdc01b7d"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"1","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":17600,"References":[]}],"MainAssetType":6,"GroupGuid":"258c7459604d085478c05930706078c2","AddressableName":"1","Labels":[],"SerializedSize":156,"StreamedSize":17600,"File":{"rid":1047},"Bundle":{"rid":1014},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1093,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-05d1c88fa8f8866205a8423046199e1e","IsSerializedFile":true,"Size":3676}},{"rid":1094,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-05d1c88fa8f8866205a8423046199e1e.resource","IsSerializedFile":false,"Size":15168}},{"rid":1095,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"f778ae0076321f44ab2c21fcaac7036e","AssetPath":"Assets/Samples/Saxophone1/9.mp3","InternalId":"Assets/Samples/Saxophone1/9.mp3","AssetHash":{"serializedVersion":"2","Hash":"044e3a1ad7bb939b1477a6fb6cf454ea"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"9","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":15168,"References":[]}],"MainAssetType":6,"GroupGuid":"258c7459604d085478c05930706078c2","AddressableName":"9","Labels":[],"SerializedSize":156,"StreamedSize":15168,"File":{"rid":1048},"Bundle":{"rid":1015},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1096,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-d021e216fd6d23c0d84b2c10d7d2fe9b","IsSerializedFile":true,"Size":3684}},{"rid":1097,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-d021e216fd6d23c0d84b2c10d7d2fe9b.resource","IsSerializedFile":false,"Size":10016}},{"rid":1098,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"05a9426e7d9e40c4987c3626716fca50","AssetPath":"Assets/Samples/Generated/drum1.mp3","InternalId":"Assets/Samples/Generated/drum1.mp3","AssetHash":{"serializedVersion":"2","Hash":"d0da3f43e8ea8ffdb231525891abd7ef"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"drum1","ComponentName":"","AssetType":6,"SerializedSize":160,"StreamedSize":10016,"References":[]}],"MainAssetType":6,"GroupGuid":"a201e34a5a3fa024080d99bcfceca817","AddressableName":"drum1","Labels":[],"SerializedSize":160,"StreamedSize":10016,"File":{"rid":1049},"Bundle":{"rid":1018},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1099,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-e02a12723316bd57c4529362df4c1c7b","IsSerializedFile":true,"Size":3688}},{"rid":1100,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-e02a12723316bd57c4529362df4c1c7b.resource","IsSerializedFile":false,"Size":585312}},{"rid":1101,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"3cda7623f7b5b9246a6061ca8229b092","AssetPath":"Assets/Samples/Generated/brownian.mp3","InternalId":"Assets/Samples/Generated/brownian.mp3","AssetHash":{"serializedVersion":"2","Hash":"8fe0a5dc26e0964f26fddc597b7d7c33"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"brownian","ComponentName":"","AssetType":6,"SerializedSize":160,"StreamedSize":585312,"References":[]}],"MainAssetType":6,"GroupGuid":"a201e34a5a3fa024080d99bcfceca817","AddressableName":"brownian","Labels":[],"SerializedSize":160,"StreamedSize":585312,"File":{"rid":1050},"Bundle":{"rid":1019},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1102,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-dfc27c99e8d3e8200d1e73d09ba342fc","IsSerializedFile":true,"Size":3696}},{"rid":1103,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-dfc27c99e8d3e8200d1e73d09ba342fc.resource","IsSerializedFile":false,"Size":5600}},{"rid":1104,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"53b75bd55183b2a45a1df4c8b8165a45","AssetPath":"Assets/Samples/Generated/sine440.mp3","InternalId":"Assets/Samples/Generated/sine440.mp3","AssetHash":{"serializedVersion":"2","Hash":"c1c1e2e7e5daff3912b3a63d6c9ea6d3"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"sine440","ComponentName":"","AssetType":6,"SerializedSize":160,"StreamedSize":5600,"References":[]}],"MainAssetType":6,"GroupGuid":"a201e34a5a3fa024080d99bcfceca817","AddressableName":"sine440","Labels":[],"SerializedSize":160,"StreamedSize":5600,"File":{"rid":1051},"Bundle":{"rid":1020},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1105,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-d8c2508dc18ced397b0b7ea3e293046b","IsSerializedFile":true,"Size":3692}},{"rid":1106,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-d8c2508dc18ced397b0b7ea3e293046b.resource","IsSerializedFile":false,"Size":76512}},{"rid":1107,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"7b71f8948f4fd38428df1a68d3f125d3","AssetPath":"Assets/Samples/Generated/sine.mp3","InternalId":"Assets/Samples/Generated/sine.mp3","AssetHash":{"serializedVersion":"2","Hash":"4be37db5520820e1da7608952615032a"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"sine","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":76512,"References":[]}],"MainAssetType":6,"GroupGuid":"a201e34a5a3fa024080d99bcfceca817","AddressableName":"sine","Labels":[],"SerializedSize":156,"StreamedSize":76512,"File":{"rid":1052},"Bundle":{"rid":1021},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1108,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-ab13095ed3bae6812ebbdefdf8c8fb30","IsSerializedFile":true,"Size":3684}},{"rid":1109,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-ab13095ed3bae6812ebbdefdf8c8fb30.resource","IsSerializedFile":false,"Size":411584}},{"rid":1110,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"b5ac7dcf38759374487178c81bfb5e08","AssetPath":"Assets/Samples/Generated/square.mp3","InternalId":"Assets/Samples/Generated/square.mp3","AssetHash":{"serializedVersion":"2","Hash":"f0b63a4867fee36ca0c25fffa6e73f91"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"square","ComponentName":"","AssetType":6,"SerializedSize":160,"StreamedSize":411584,"References":[]}],"MainAssetType":6,"GroupGuid":"a201e34a5a3fa024080d99bcfceca817","AddressableName":"square","Labels":[],"SerializedSize":160,"StreamedSize":411584,"File":{"rid":1053},"Bundle":{"rid":1022},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1111,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-6d55f28146ef170678a8f3a3057434a1","IsSerializedFile":true,"Size":3704}},{"rid":1112,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-6d55f28146ef170678a8f3a3057434a1.resource","IsSerializedFile":false,"Size":50848}},{"rid":1113,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"c2dc13a18b5d4aa45a78f010cf27e471","AssetPath":"Assets/Samples/Generated/square300.mp3","InternalId":"Assets/Samples/Generated/square300.mp3","AssetHash":{"serializedVersion":"2","Hash":"20e643e7899e9d8dc0a995d60373d1d1"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"square300","ComponentName":"","AssetType":6,"SerializedSize":164,"StreamedSize":50848,"References":[]}],"MainAssetType":6,"GroupGuid":"a201e34a5a3fa024080d99bcfceca817","AddressableName":"square300","Labels":[],"SerializedSize":164,"StreamedSize":50848,"File":{"rid":1054},"Bundle":{"rid":1023},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1114,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-2d0e0b283a1fb1a45b272552509552ea","IsSerializedFile":true,"Size":3684}},{"rid":1115,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-2d0e0b283a1fb1a45b272552509552ea.resource","IsSerializedFile":false,"Size":10176}},{"rid":1116,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"d9cbbbe6d1a30f248a799b211d6fbddc","AssetPath":"Assets/Samples/Generated/drum2.mp3","InternalId":"Assets/Samples/Generated/drum2.mp3","AssetHash":{"serializedVersion":"2","Hash":"aa796e94d85494f1022d199420eb00e9"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"drum2","ComponentName":"","AssetType":6,"SerializedSize":160,"StreamedSize":10176,"References":[]}],"MainAssetType":6,"GroupGuid":"a201e34a5a3fa024080d99bcfceca817","AddressableName":"drum2","Labels":[],"SerializedSize":160,"StreamedSize":10176,"File":{"rid":1055},"Bundle":{"rid":1024},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1117,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-2a6b5cdb2a206a3d4aa0eab375f1e5d6","IsSerializedFile":true,"Size":3676}},{"rid":1118,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-2a6b5cdb2a206a3d4aa0eab375f1e5d6.resource","IsSerializedFile":false,"Size":69536}},{"rid":1119,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"196f8c9e3e9cc764db55da751c1b5d9d","AssetPath":"Assets/Samples/Saxophone2/e.mp3","InternalId":"Assets/Samples/Saxophone2/e.mp3","AssetHash":{"serializedVersion":"2","Hash":"595975d07d7c55461eaefd0cb68f39ab"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"e","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":69536,"References":[]}],"MainAssetType":6,"GroupGuid":"a56c5fee201ac7840a927f1c63a68728","AddressableName":"e","Labels":[],"SerializedSize":156,"StreamedSize":69536,"File":{"rid":1056},"Bundle":{"rid":1027},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1120,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-a209ca6fc6e41ac9f6d6fc1d57dc96e3","IsSerializedFile":true,"Size":3676}},{"rid":1121,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-a209ca6fc6e41ac9f6d6fc1d57dc96e3.resource","IsSerializedFile":false,"Size":17280}},{"rid":1122,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"3d88ab3314ac1c9438e0f35a59507386","AssetPath":"Assets/Samples/Saxophone2/c.mp3","InternalId":"Assets/Samples/Saxophone2/c.mp3","AssetHash":{"serializedVersion":"2","Hash":"186d0444728feeea6897392ad1b23646"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"c","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":17280,"References":[]}],"MainAssetType":6,"GroupGuid":"a56c5fee201ac7840a927f1c63a68728","AddressableName":"c","Labels":[],"SerializedSize":156,"StreamedSize":17280,"File":{"rid":1057},"Bundle":{"rid":1028},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1123,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-5319be78ac1d58d02d94d24bec324ca6","IsSerializedFile":true,"Size":3676}},{"rid":1124,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-5319be78ac1d58d02d94d24bec324ca6.resource","IsSerializedFile":false,"Size":59136}},{"rid":1125,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"4016dace81140b54ea0671c1720a43e3","AssetPath":"Assets/Samples/Saxophone2/j.mp3","InternalId":"Assets/Samples/Saxophone2/j.mp3","AssetHash":{"serializedVersion":"2","Hash":"d560bc884e64301ded4ab20f0c6eacb0"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"j","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":59136,"References":[]}],"MainAssetType":6,"GroupGuid":"a56c5fee201ac7840a927f1c63a68728","AddressableName":"j","Labels":[],"SerializedSize":156,"StreamedSize":59136,"File":{"rid":1058},"Bundle":{"rid":1029},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1126,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-8cc6bc79beeec25a3b2e09484debb7bf","IsSerializedFile":true,"Size":3680}},{"rid":1127,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-8cc6bc79beeec25a3b2e09484debb7bf.resource","IsSerializedFile":false,"Size":67136}},{"rid":1128,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"7939bd4f5f2fbdf49bafc5670ab6effa","AssetPath":"Assets/Samples/Saxophone2/h.mp3","InternalId":"Assets/Samples/Saxophone2/h.mp3","AssetHash":{"serializedVersion":"2","Hash":"1819624ce488110ca05f21f6f0d20afd"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"h","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":67136,"References":[]}],"MainAssetType":6,"GroupGuid":"a56c5fee201ac7840a927f1c63a68728","AddressableName":"h","Labels":[],"SerializedSize":156,"StreamedSize":67136,"File":{"rid":1059},"Bundle":{"rid":1030},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1129,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-e74367794c024c21ece7e1aa44e43744","IsSerializedFile":true,"Size":3676}},{"rid":1130,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-e74367794c024c21ece7e1aa44e43744.resource","IsSerializedFile":false,"Size":50688}},{"rid":1131,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"88339cf287767ec418310d09f36890d7","AssetPath":"Assets/Samples/Saxophone2/f.mp3","InternalId":"Assets/Samples/Saxophone2/f.mp3","AssetHash":{"serializedVersion":"2","Hash":"021fef13a6a5c26bb478c97d61593d50"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"f","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":50688,"References":[]}],"MainAssetType":6,"GroupGuid":"a56c5fee201ac7840a927f1c63a68728","AddressableName":"f","Labels":[],"SerializedSize":156,"StreamedSize":50688,"File":{"rid":1060},"Bundle":{"rid":1031},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1132,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-84aa7474c89cce2aa8ded8d7683cb357","IsSerializedFile":true,"Size":3680}},{"rid":1133,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-84aa7474c89cce2aa8ded8d7683cb357.resource","IsSerializedFile":false,"Size":19648}},{"rid":1134,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"8fb0886c8a099dd41b90c45f7661a985","AssetPath":"Assets/Samples/Saxophone2/b.mp3","InternalId":"Assets/Samples/Saxophone2/b.mp3","AssetHash":{"serializedVersion":"2","Hash":"8cf6772ae65b8cdd0f61b49192040aab"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"b","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":19648,"References":[]}],"MainAssetType":6,"GroupGuid":"a56c5fee201ac7840a927f1c63a68728","AddressableName":"b","Labels":[],"SerializedSize":156,"StreamedSize":19648,"File":{"rid":1061},"Bundle":{"rid":1032},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1135,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-a2439cd83acd1c60c58561542cf99432","IsSerializedFile":true,"Size":3676}},{"rid":1136,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-a2439cd83acd1c60c58561542cf99432.resource","IsSerializedFile":false,"Size":130624}},{"rid":1137,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"a07c3f4745fae9045a615773fd208e27","AssetPath":"Assets/Samples/Saxophone2/d.mp3","InternalId":"Assets/Samples/Saxophone2/d.mp3","AssetHash":{"serializedVersion":"2","Hash":"2fbfe1f4d362be7e6f7c1ba931ec44e6"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"d","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":130624,"References":[]}],"MainAssetType":6,"GroupGuid":"a56c5fee201ac7840a927f1c63a68728","AddressableName":"d","Labels":[],"SerializedSize":156,"StreamedSize":130624,"File":{"rid":1062},"Bundle":{"rid":1033},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1138,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-fbb9c1b8073b821e042a5bc47ba09cf7","IsSerializedFile":true,"Size":3680}},{"rid":1139,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-fbb9c1b8073b821e042a5bc47ba09cf7.resource","IsSerializedFile":false,"Size":47424}},{"rid":1140,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"b65a7916245593b4e89f4bd0aa920533","AssetPath":"Assets/Samples/Saxophone2/a.mp3","InternalId":"Assets/Samples/Saxophone2/a.mp3","AssetHash":{"serializedVersion":"2","Hash":"ef69479f5ac11a5908709f854667429a"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"a","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":47424,"References":[]}],"MainAssetType":6,"GroupGuid":"a56c5fee201ac7840a927f1c63a68728","AddressableName":"a","Labels":[],"SerializedSize":156,"StreamedSize":47424,"File":{"rid":1063},"Bundle":{"rid":1034},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1141,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-f6c891248d11a37872b027b369b820af","IsSerializedFile":true,"Size":3680}},{"rid":1142,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-f6c891248d11a37872b027b369b820af.resource","IsSerializedFile":false,"Size":75616}},{"rid":1143,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"bc3e0ce7464020d4f8a320e0b4f1fb2a","AssetPath":"Assets/Samples/Saxophone2/i.mp3","InternalId":"Assets/Samples/Saxophone2/i.mp3","AssetHash":{"serializedVersion":"2","Hash":"39e237b4cf5a5dcabf21590d8f8c31ce"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"i","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":75616,"References":[]}],"MainAssetType":6,"GroupGuid":"a56c5fee201ac7840a927f1c63a68728","AddressableName":"i","Labels":[],"SerializedSize":156,"StreamedSize":75616,"File":{"rid":1064},"Bundle":{"rid":1035},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1144,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-2c2d6e9b78804ee1e800e2b1c620f98a","IsSerializedFile":true,"Size":3680}},{"rid":1145,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-2c2d6e9b78804ee1e800e2b1c620f98a.resource","IsSerializedFile":false,"Size":61248}},{"rid":1146,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"dd6d9bdc76d97ad45ab51e7706ca1fe6","AssetPath":"Assets/Samples/Saxophone2/g.mp3","InternalId":"Assets/Samples/Saxophone2/g.mp3","AssetHash":{"serializedVersion":"2","Hash":"bc56a2fe480c324ea70660c567d7efd2"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"g","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":61248,"References":[]}],"MainAssetType":6,"GroupGuid":"a56c5fee201ac7840a927f1c63a68728","AddressableName":"g","Labels":[],"SerializedSize":156,"StreamedSize":61248,"File":{"rid":1065},"Bundle":{"rid":1036},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}}]}} \ No newline at end of file diff --git a/TestCommon/Data/AddressableBuildLayouts/buildlayout_2025.01.28.16.51.14.json b/TestCommon/Data/AddressableBuildLayouts/buildlayout_2025.01.28.16.51.14.json new file mode 100644 index 0000000..27c3e76 --- /dev/null +++ b/TestCommon/Data/AddressableBuildLayouts/buildlayout_2025.01.28.16.51.14.json @@ -0,0 +1,2 @@ +{"BuildTarget":19,"BuildResultHash":"a762437258c504cc7f18518dc9663c2b","BuildType":0,"BuildStartTime":"1/28/2025 4:51:14 PM","Duration":4.139456399999999,"BuildError":"", +"UnityVersion":"6000.1.0b2","PackageVersion":"com.unity.addressables: 2.2.2","PlayerBuildVersion":"1.0","AddressablesEditorSettings":{"SettingsHash":"fca389814f9353d943910c8b1052b491","ActiveProfile":{"Name":"Default","Id":"eb1018326f70bd440b0fda653327eb37","Values":[{"Key":"027d618fc8b846c43bde4f69b94d6548","Value":"ServerData/[BuildTarget]"},{"Key":"091eb06bd685ad44b84643456262ddb9","Value":""},{"Key":"69a2bcc4bc4078d4d81a27ccb79d293d","Value":"[UnityEngine.AddressableAssets.Addressables.BuildPath]/[BuildTarget]"},{"Key":"8867b9b32191a4a48bc3deccf931a225","Value":"[UnityEditor.EditorUserBuildSettings.activeBuildTarget]"},{"Key":"db7c73bb3ea77cd468fe4a345d8f45be","Value":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}/[BuildTarget]"}]},"BuildRemoteCatalog":false,"RemoteCatalogLoadPath":"","BundleLocalCatalog":false,"OptimizeCatalogSize":false,"CatalogRequestsTimeout":0,"MaxConcurrentWebRequests":3,"DisableCatalogUpdateOnStartup":false,"UniqueBundleIds":false,"EnableJsonCatalog":false,"NonRecursiveBuilding":true,"ContiguousBundles":true,"DisableSubAssetRepresentations":false,"ShaderBundleNaming":"ProjectName","MonoScriptBundleNaming":"ProjectName","StripUnityVersionFromBundleBuild":false},"AddressablesRuntimeSettings":{"LogResourceManagerExceptions":true,"CatalogLoadPaths":["{UnityEngine.AddressableAssets.Addressables.RuntimePath}/catalog.bin"],"CatalogHash":"edb7d8981197e6c70970c195c741f94f"},"BuildScript":"Default Build Script","DefaultGroup":{"rid":1000},"Groups":[{"rid":1001},{"rid":1000},{"rid":1002},{"rid":1003}],"BuiltInBundles":[],"DuplicatedAssets":[],"LocalCatalogBuildPath":"Library/com.unity.addressables/aa/Windows/StandaloneWindows64","RemoteCatalogBuildPath":"","references":{"version":2,"RefIds":[{"rid":1000,"type":{"class":"BuildLayout/Group","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"Default Local Group","Guid":"336a756d05aafc24295e5bd4d29d32fd","PackingMode":"PackTogether","Bundles":[],"Schemas":[{"rid":1004},{"rid":1005}]}},{"rid":1001,"type":{"class":"BuildLayout/Group","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"SamplePack1","Guid":"258c7459604d085478c05930706078c2","PackingMode":"PackSeparately","Bundles":[{"rid":1006},{"rid":1007},{"rid":1008},{"rid":1009},{"rid":1010},{"rid":1011},{"rid":1012},{"rid":1013},{"rid":1014},{"rid":1015}],"Schemas":[{"rid":1016},{"rid":1017}]}},{"rid":1002,"type":{"class":"BuildLayout/Group","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"Generated","Guid":"a201e34a5a3fa024080d99bcfceca817","PackingMode":"PackSeparately","Bundles":[{"rid":1018},{"rid":1019},{"rid":1020},{"rid":1021},{"rid":1022},{"rid":1023},{"rid":1024}],"Schemas":[{"rid":1025},{"rid":1026}]}},{"rid":1003,"type":{"class":"BuildLayout/Group","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"SamplePack2","Guid":"a56c5fee201ac7840a927f1c63a68728","PackingMode":"PackSeparately","Bundles":[{"rid":1027},{"rid":1028},{"rid":1029},{"rid":1030},{"rid":1031},{"rid":1032},{"rid":1033},{"rid":1034},{"rid":1035},{"rid":1036}],"Schemas":[{"rid":1037},{"rid":1038}]}},{"rid":1004,"type":{"class":"BuildLayout/SchemaData","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"59cf7bcd70ab1b04c9384d55c7283117","Type":"BundledAssetGroupSchema","SchemaDataPairs":[{"Key":"InternalBundleIdMode","Value":"GroupGuidProjectIdHash"},{"Key":"Compression","Value":"LZ4"},{"Key":"IncludeAddressInCatalog","Value":"True"},{"Key":"IncludeGUIDInCatalog","Value":"True"},{"Key":"IncludeLabelsInCatalog","Value":"True"},{"Key":"InternalIdNamingMode","Value":"FullPath"},{"Key":"AssetBundledCacheClearBehavior","Value":"ClearWhenSpaceIsNeededInCache"},{"Key":"IncludeInBuild","Value":"True"},{"Key":"BundledAssetProviderType","Value":"UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider"},{"Key":"ForceUniqueProvider","Value":"False"},{"Key":"UseAssetBundleCache","Value":"True"},{"Key":"UseAssetBundleCrc","Value":"True"},{"Key":"UseAssetBundleCrcForCachedBundles","Value":"True"},{"Key":"UseUnityWebRequestForLocalBundles","Value":"False"},{"Key":"Timeout","Value":"0"},{"Key":"ChunkedTransfer","Value":"False"},{"Key":"RedirectLimit","Value":"-1"},{"Key":"RetryCount","Value":"0"},{"Key":"BuildPath","Value":"Library/com.unity.addressables/aa/Windows/StandaloneWindows64"},{"Key":"LoadPath","Value":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}/StandaloneWindows64"},{"Key":"PackingMode","Value":"PackTogether"},{"Key":"AssetBundleProviderType","Value":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider"},{"Key":"UseDefaultSchemaSettings","Value":"False"},{"Key":"SelectedPathPairIndex","Value":"0"},{"Key":"BundleNaming","Value":"AppendHash"},{"Key":"AssetLoadMode","Value":"RequestedAssetAndDependencies"}]}},{"rid":1005,"type":{"class":"BuildLayout/SchemaData","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"b819cb823fb33024bbd3ac2f201858d8","Type":"ContentUpdateGroupSchema","SchemaDataPairs":[{"Key":"StaticContent","Value":"False"}]}},{"rid":1006,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack1_assets_4.bundle","InternalName":"1f8d4b3797eb32be56cfe202caec6fd6","FileSize":23040,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":3983860733,"Hash":{"serializedVersion":"2","Hash":"8537ba5e4a59fcb3afad3b35878f92b5"},"Group":{"rid":1001},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack1_assets_4.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1039}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1007,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack1_assets_6.bundle","InternalName":"3b83c8d34ca68f6e8852ceeb09c1eac4","FileSize":34528,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":4276430644,"Hash":{"serializedVersion":"2","Hash":"ac97b5cd2d8c21ca5ce5b9f37d83bdd8"},"Group":{"rid":1001},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack1_assets_6.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1040}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1008,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack1_assets_3.bundle","InternalName":"1ff936ac860951602e79522deaa8c9c6","FileSize":25920,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":3072052095,"Hash":{"serializedVersion":"2","Hash":"2f713cc6c5ac93944787e53abc171506"},"Group":{"rid":1001},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack1_assets_3.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1041}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1009,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack1_assets_5.bundle","InternalName":"69cc943b938e112fdb596763b7e3325c","FileSize":22336,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":1442816670,"Hash":{"serializedVersion":"2","Hash":"deaa6a181faf7460597c59a1ccca07ab"},"Group":{"rid":1001},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack1_assets_5.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1042}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1010,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack1_assets_2.bundle","InternalName":"6b51b282e9b4dd13e69d0739a380a55b","FileSize":22624,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":63243828,"Hash":{"serializedVersion":"2","Hash":"3a6d4700474b3c0f8d50dfe7cff32fef"},"Group":{"rid":1001},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack1_assets_2.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1043}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1011,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack1_assets_7.bundle","InternalName":"175e752b232a665bd379dadef49c596c","FileSize":18816,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":1064022522,"Hash":{"serializedVersion":"2","Hash":"1b40b4f0c23bf37432b5adcefce3bfe6"},"Group":{"rid":1001},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack1_assets_7.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1044}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1012,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack1_assets_0.bundle","InternalName":"9b397865eb247e7fd24573450180e68a","FileSize":33824,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":1395461927,"Hash":{"serializedVersion":"2","Hash":"1643a503052be3f585a8f1813952aa2e"},"Group":{"rid":1001},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack1_assets_0.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1045}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1013,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack1_assets_8.bundle","InternalName":"81bd30248e70a514207fe576ea8be326","FileSize":15264,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":1715313454,"Hash":{"serializedVersion":"2","Hash":"4d35b226b8f9624a4105997b26d8aa7d"},"Group":{"rid":1001},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack1_assets_8.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1046}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1014,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack1_assets_1.bundle","InternalName":"6842397c643be2ce3b57ace165f4865d","FileSize":21440,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":1296609683,"Hash":{"serializedVersion":"2","Hash":"ab22469cbd3dbdfdc5aec3d50fc1ea9a"},"Group":{"rid":1001},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack1_assets_1.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1047}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1015,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack1_assets_9.bundle","InternalName":"bf7bc97bd4ced20925b9bd0e81f2d522","FileSize":19008,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":2442388881,"Hash":{"serializedVersion":"2","Hash":"1b7797b09d636b30e451e3509f17085f"},"Group":{"rid":1001},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack1_assets_9.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1048}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1016,"type":{"class":"BuildLayout/SchemaData","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"c57adc9566661284e96157fafecd9441","Type":"BundledAssetGroupSchema","SchemaDataPairs":[{"Key":"InternalBundleIdMode","Value":"GroupGuidProjectIdHash"},{"Key":"Compression","Value":"Uncompressed"},{"Key":"IncludeAddressInCatalog","Value":"True"},{"Key":"IncludeGUIDInCatalog","Value":"True"},{"Key":"IncludeLabelsInCatalog","Value":"True"},{"Key":"InternalIdNamingMode","Value":"FullPath"},{"Key":"AssetBundledCacheClearBehavior","Value":"ClearWhenSpaceIsNeededInCache"},{"Key":"IncludeInBuild","Value":"True"},{"Key":"BundledAssetProviderType","Value":"UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider"},{"Key":"ForceUniqueProvider","Value":"False"},{"Key":"UseAssetBundleCache","Value":"True"},{"Key":"UseAssetBundleCrc","Value":"True"},{"Key":"UseAssetBundleCrcForCachedBundles","Value":"True"},{"Key":"UseUnityWebRequestForLocalBundles","Value":"False"},{"Key":"Timeout","Value":"0"},{"Key":"ChunkedTransfer","Value":"False"},{"Key":"RedirectLimit","Value":"-1"},{"Key":"RetryCount","Value":"0"},{"Key":"BuildPath","Value":"Library/com.unity.addressables/aa/Windows/StandaloneWindows64"},{"Key":"LoadPath","Value":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}/StandaloneWindows64"},{"Key":"PackingMode","Value":"PackSeparately"},{"Key":"AssetBundleProviderType","Value":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider"},{"Key":"UseDefaultSchemaSettings","Value":"False"},{"Key":"SelectedPathPairIndex","Value":"0"},{"Key":"BundleNaming","Value":"NoHash"},{"Key":"AssetLoadMode","Value":"RequestedAssetAndDependencies"}]}},{"rid":1017,"type":{"class":"BuildLayout/SchemaData","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"c50c0af57295b8c42bb5d7d31526d100","Type":"ContentUpdateGroupSchema","SchemaDataPairs":[{"Key":"StaticContent","Value":"False"}]}},{"rid":1018,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"generated_assets_drum1.bundle","InternalName":"eb15f892bfc01dd4aa0ece86f7c590cb","FileSize":13872,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":694414153,"Hash":{"serializedVersion":"2","Hash":"fbfc3eaefa459f1a8dc4c47944041e49"},"Group":{"rid":1002},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\generated_assets_drum1.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1049}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1019,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"generated_assets_brownian.bundle","InternalName":"4f7dd1966bff4c80539f0a11bfb2f604","FileSize":589168,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":1844209987,"Hash":{"serializedVersion":"2","Hash":"5fbf9ee0ebb26d30d3cc0d679905be55"},"Group":{"rid":1002},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\generated_assets_brownian.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1050}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1020,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"generated_assets_sine440.bundle","InternalName":"263ef2f877d5a34b7df03dd3b5bbeac4","FileSize":9456,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":1802149312,"Hash":{"serializedVersion":"2","Hash":"3f0088ca1daf7287c0a6d442d28ab16c"},"Group":{"rid":1002},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\generated_assets_sine440.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1051}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1021,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"generated_assets_sine.bundle","InternalName":"b27a60ba4c0ec77b926f8c00dda0f250","FileSize":80368,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":632252596,"Hash":{"serializedVersion":"2","Hash":"b12d2962cea07e4ccad683f1cd676b2f"},"Group":{"rid":1002},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\generated_assets_sine.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1052}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1022,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"generated_assets_square.bundle","InternalName":"0458b4a804a69eb9a2a1833017035327","FileSize":415440,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":3072331629,"Hash":{"serializedVersion":"2","Hash":"107dfced85b01503feaa82ace8c8eb6e"},"Group":{"rid":1002},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\generated_assets_square.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1053}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1023,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"generated_assets_square300.bundle","InternalName":"440d49ce6d183b7c229bfa22d06bb0c9","FileSize":54720,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":2355528378,"Hash":{"serializedVersion":"2","Hash":"ca4dba1b947d28a787c26d35eaaaf1cc"},"Group":{"rid":1002},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\generated_assets_square300.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1054}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1024,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"generated_assets_drum2.bundle","InternalName":"1cbb611c300205dd4ff1d1a4062715cd","FileSize":14032,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":4195441782,"Hash":{"serializedVersion":"2","Hash":"389ddf22f270063e7e15b2854e7e645d"},"Group":{"rid":1002},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\generated_assets_drum2.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1055}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1025,"type":{"class":"BuildLayout/SchemaData","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"4078d0d3d05126f4ca5d1809fb9865b0","Type":"BundledAssetGroupSchema","SchemaDataPairs":[{"Key":"InternalBundleIdMode","Value":"GroupGuidProjectIdHash"},{"Key":"Compression","Value":"Uncompressed"},{"Key":"IncludeAddressInCatalog","Value":"True"},{"Key":"IncludeGUIDInCatalog","Value":"True"},{"Key":"IncludeLabelsInCatalog","Value":"True"},{"Key":"InternalIdNamingMode","Value":"FullPath"},{"Key":"AssetBundledCacheClearBehavior","Value":"ClearWhenSpaceIsNeededInCache"},{"Key":"IncludeInBuild","Value":"True"},{"Key":"BundledAssetProviderType","Value":"UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider"},{"Key":"ForceUniqueProvider","Value":"False"},{"Key":"UseAssetBundleCache","Value":"True"},{"Key":"UseAssetBundleCrc","Value":"True"},{"Key":"UseAssetBundleCrcForCachedBundles","Value":"True"},{"Key":"UseUnityWebRequestForLocalBundles","Value":"False"},{"Key":"Timeout","Value":"0"},{"Key":"ChunkedTransfer","Value":"False"},{"Key":"RedirectLimit","Value":"-1"},{"Key":"RetryCount","Value":"0"},{"Key":"BuildPath","Value":"Library/com.unity.addressables/aa/Windows/StandaloneWindows64"},{"Key":"LoadPath","Value":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}/StandaloneWindows64"},{"Key":"PackingMode","Value":"PackSeparately"},{"Key":"AssetBundleProviderType","Value":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider"},{"Key":"UseDefaultSchemaSettings","Value":"False"},{"Key":"SelectedPathPairIndex","Value":"0"},{"Key":"BundleNaming","Value":"NoHash"},{"Key":"AssetLoadMode","Value":"RequestedAssetAndDependencies"}]}},{"rid":1026,"type":{"class":"BuildLayout/SchemaData","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"7550134532116b546bbfc73d635d238d","Type":"ContentUpdateGroupSchema","SchemaDataPairs":[{"Key":"StaticContent","Value":"False"}]}},{"rid":1027,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack2_assets_e.bundle","InternalName":"6474f377f608a8c16ff891aed86ad9c6","FileSize":73376,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":4278687744,"Hash":{"serializedVersion":"2","Hash":"e73641e44d76286924c42a4869ab1dd1"},"Group":{"rid":1003},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack2_assets_e.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1056}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1028,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack2_assets_c.bundle","InternalName":"3cd71b90318e38bf58581044d4347d69","FileSize":21120,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":1624109646,"Hash":{"serializedVersion":"2","Hash":"31d562de9449e50f1bbc6496696f4ea1"},"Group":{"rid":1003},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack2_assets_c.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1057}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1029,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack2_assets_j.bundle","InternalName":"b64b402edb473d5183e9e428f8a744a2","FileSize":62976,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":3241181028,"Hash":{"serializedVersion":"2","Hash":"93e2176fdb1e52c234611e32440653b6"},"Group":{"rid":1003},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack2_assets_j.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1058}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1030,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack2_assets_h.bundle","InternalName":"3b16fc37c6221a87f82c9aadd34e7af4","FileSize":70976,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":1058037547,"Hash":{"serializedVersion":"2","Hash":"130c21f4f6fab54624f9929178da838d"},"Group":{"rid":1003},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack2_assets_h.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1059}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1031,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack2_assets_f.bundle","InternalName":"5e3b8d40fbcd8f3c3b4e8eb144de6af4","FileSize":54528,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":1251071412,"Hash":{"serializedVersion":"2","Hash":"e02ddc8784b1ead17d021bc3f7704ed3"},"Group":{"rid":1003},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack2_assets_f.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1060}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1032,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack2_assets_b.bundle","InternalName":"cc6a2013cf6341e142c7a2937d22b6b8","FileSize":23488,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":3745088177,"Hash":{"serializedVersion":"2","Hash":"72fd915bc88a0e3122bdf8410d84498b"},"Group":{"rid":1003},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack2_assets_b.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1061}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1033,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack2_assets_d.bundle","InternalName":"b9872cdeb1f364a3531aca1beeae2c77","FileSize":134464,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":650826451,"Hash":{"serializedVersion":"2","Hash":"e2b46d4caabe1bfd5ec92115bff593fa"},"Group":{"rid":1003},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack2_assets_d.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1062}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1034,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack2_assets_a.bundle","InternalName":"30ae8f6391be92f8f247d3c771fb2c1e","FileSize":51264,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":597684922,"Hash":{"serializedVersion":"2","Hash":"4bdff88ddff01b857c9f2584d4c73fbe"},"Group":{"rid":1003},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack2_assets_a.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1063}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1035,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack2_assets_i.bundle","InternalName":"c5ea5ba50ff6c704382d42bff195c10e","FileSize":79456,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":857134522,"Hash":{"serializedVersion":"2","Hash":"d1e23b53f215bf4997133b70f80f3634"},"Group":{"rid":1003},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack2_assets_i.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1064}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1036,"type":{"class":"BuildLayout/Bundle","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"samplepack2_assets_g.bundle","InternalName":"bb3e6d6039b26a029b9c7cb8edaa8e2a","FileSize":65088,"BuildStatus":0,"ExpandedDependencyFileSize":0,"DependencyFileSize":0,"AssetCount":1,"BundleDependencies":[],"Compression":"None","CRC":2918875995,"Hash":{"serializedVersion":"2","Hash":"ee3c751b3c0b5649c00d41d0728752b5"},"Group":{"rid":1003},"LoadPath":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\samplepack2_assets_g.bundle","Provider":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider","ResultType":"IAssetBundleResource","Files":[{"rid":1065}],"DependentBundles":[],"Dependencies":[],"ExpandedDependencies":[]}},{"rid":1037,"type":{"class":"BuildLayout/SchemaData","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"a5af9a2aae0a45347a8a5ef8fafece02","Type":"BundledAssetGroupSchema","SchemaDataPairs":[{"Key":"InternalBundleIdMode","Value":"GroupGuidProjectIdHash"},{"Key":"Compression","Value":"Uncompressed"},{"Key":"IncludeAddressInCatalog","Value":"True"},{"Key":"IncludeGUIDInCatalog","Value":"True"},{"Key":"IncludeLabelsInCatalog","Value":"True"},{"Key":"InternalIdNamingMode","Value":"FullPath"},{"Key":"AssetBundledCacheClearBehavior","Value":"ClearWhenSpaceIsNeededInCache"},{"Key":"IncludeInBuild","Value":"True"},{"Key":"BundledAssetProviderType","Value":"UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider"},{"Key":"ForceUniqueProvider","Value":"False"},{"Key":"UseAssetBundleCache","Value":"True"},{"Key":"UseAssetBundleCrc","Value":"True"},{"Key":"UseAssetBundleCrcForCachedBundles","Value":"True"},{"Key":"UseUnityWebRequestForLocalBundles","Value":"False"},{"Key":"Timeout","Value":"0"},{"Key":"ChunkedTransfer","Value":"False"},{"Key":"RedirectLimit","Value":"-1"},{"Key":"RetryCount","Value":"0"},{"Key":"BuildPath","Value":"Library/com.unity.addressables/aa/Windows/StandaloneWindows64"},{"Key":"LoadPath","Value":"{UnityEngine.AddressableAssets.Addressables.RuntimePath}/StandaloneWindows64"},{"Key":"PackingMode","Value":"PackSeparately"},{"Key":"AssetBundleProviderType","Value":"UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider"},{"Key":"UseDefaultSchemaSettings","Value":"False"},{"Key":"SelectedPathPairIndex","Value":"0"},{"Key":"BundleNaming","Value":"NoHash"},{"Key":"AssetLoadMode","Value":"RequestedAssetAndDependencies"}]}},{"rid":1038,"type":{"class":"BuildLayout/SchemaData","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"c0dbabc9705c9644d9a44d00bd653470","Type":"ContentUpdateGroupSchema","SchemaDataPairs":[{"Key":"StaticContent","Value":"False"}]}},{"rid":1039,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-73b98a43ee020a5e591db9518c2cb296/CAB-73b98a43ee020a5e591db9518c2cb296","Bundle":{"rid":1006},"SubFiles":[{"rid":1066},{"rid":1067}],"Assets":[{"rid":1068}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1040,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-27752acd531d26b983a15c00264c45fb/CAB-27752acd531d26b983a15c00264c45fb","Bundle":{"rid":1007},"SubFiles":[{"rid":1069},{"rid":1070}],"Assets":[{"rid":1071}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1041,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-1f5bcdc7a5409950f693976900dd4aa2/CAB-1f5bcdc7a5409950f693976900dd4aa2","Bundle":{"rid":1008},"SubFiles":[{"rid":1072},{"rid":1073}],"Assets":[{"rid":1074}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1042,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-0fa1a01f0e1b970a85c89e575595f749/CAB-0fa1a01f0e1b970a85c89e575595f749","Bundle":{"rid":1009},"SubFiles":[{"rid":1075},{"rid":1076}],"Assets":[{"rid":1077}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1043,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-01315ab73001d0050a9e9a4454dc947f/CAB-01315ab73001d0050a9e9a4454dc947f","Bundle":{"rid":1010},"SubFiles":[{"rid":1078},{"rid":1079}],"Assets":[{"rid":1080}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1044,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-33b13439bdd10f67ea9ad546608069c7/CAB-33b13439bdd10f67ea9ad546608069c7","Bundle":{"rid":1011},"SubFiles":[{"rid":1081},{"rid":1082}],"Assets":[{"rid":1083}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1045,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-70392fbb89818a9b1dc85933205fc393/CAB-70392fbb89818a9b1dc85933205fc393","Bundle":{"rid":1012},"SubFiles":[{"rid":1084},{"rid":1085}],"Assets":[{"rid":1086}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1046,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-0779c46a926a678e417643586b001645/CAB-0779c46a926a678e417643586b001645","Bundle":{"rid":1013},"SubFiles":[{"rid":1087},{"rid":1088}],"Assets":[{"rid":1089}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1047,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-0a2912740c976ca7a7b1ccc28e6e7a7a/CAB-0a2912740c976ca7a7b1ccc28e6e7a7a","Bundle":{"rid":1014},"SubFiles":[{"rid":1090},{"rid":1091}],"Assets":[{"rid":1092}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1048,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-05d1c88fa8f8866205a8423046199e1e/CAB-05d1c88fa8f8866205a8423046199e1e","Bundle":{"rid":1015},"SubFiles":[{"rid":1093},{"rid":1094}],"Assets":[{"rid":1095}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1049,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-d021e216fd6d23c0d84b2c10d7d2fe9b/CAB-d021e216fd6d23c0d84b2c10d7d2fe9b","Bundle":{"rid":1018},"SubFiles":[{"rid":1096},{"rid":1097}],"Assets":[{"rid":1098}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":212},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1050,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-e02a12723316bd57c4529362df4c1c7b/CAB-e02a12723316bd57c4529362df4c1c7b","Bundle":{"rid":1019},"SubFiles":[{"rid":1099},{"rid":1100}],"Assets":[{"rid":1101}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":216},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1051,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-dfc27c99e8d3e8200d1e73d09ba342fc/CAB-dfc27c99e8d3e8200d1e73d09ba342fc","Bundle":{"rid":1020},"SubFiles":[{"rid":1102},{"rid":1103}],"Assets":[{"rid":1104}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":212},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1052,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-d8c2508dc18ced397b0b7ea3e293046b/CAB-d8c2508dc18ced397b0b7ea3e293046b","Bundle":{"rid":1021},"SubFiles":[{"rid":1105},{"rid":1106}],"Assets":[{"rid":1107}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":212},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1053,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-ab13095ed3bae6812ebbdefdf8c8fb30/CAB-ab13095ed3bae6812ebbdefdf8c8fb30","Bundle":{"rid":1022},"SubFiles":[{"rid":1108},{"rid":1109}],"Assets":[{"rid":1110}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":212},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1054,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-6d55f28146ef170678a8f3a3057434a1/CAB-6d55f28146ef170678a8f3a3057434a1","Bundle":{"rid":1023},"SubFiles":[{"rid":1111},{"rid":1112}],"Assets":[{"rid":1113}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":216},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1055,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-2d0e0b283a1fb1a45b272552509552ea/CAB-2d0e0b283a1fb1a45b272552509552ea","Bundle":{"rid":1024},"SubFiles":[{"rid":1114},{"rid":1115}],"Assets":[{"rid":1116}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":212},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1056,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-2a6b5cdb2a206a3d4aa0eab375f1e5d6/CAB-2a6b5cdb2a206a3d4aa0eab375f1e5d6","Bundle":{"rid":1027},"SubFiles":[{"rid":1117},{"rid":1118}],"Assets":[{"rid":1119}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1057,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-a209ca6fc6e41ac9f6d6fc1d57dc96e3/CAB-a209ca6fc6e41ac9f6d6fc1d57dc96e3","Bundle":{"rid":1028},"SubFiles":[{"rid":1120},{"rid":1121}],"Assets":[{"rid":1122}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1058,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-5319be78ac1d58d02d94d24bec324ca6/CAB-5319be78ac1d58d02d94d24bec324ca6","Bundle":{"rid":1029},"SubFiles":[{"rid":1123},{"rid":1124}],"Assets":[{"rid":1125}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1059,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-8cc6bc79beeec25a3b2e09484debb7bf/CAB-8cc6bc79beeec25a3b2e09484debb7bf","Bundle":{"rid":1030},"SubFiles":[{"rid":1126},{"rid":1127}],"Assets":[{"rid":1128}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1060,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-e74367794c024c21ece7e1aa44e43744/CAB-e74367794c024c21ece7e1aa44e43744","Bundle":{"rid":1031},"SubFiles":[{"rid":1129},{"rid":1130}],"Assets":[{"rid":1131}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1061,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-84aa7474c89cce2aa8ded8d7683cb357/CAB-84aa7474c89cce2aa8ded8d7683cb357","Bundle":{"rid":1032},"SubFiles":[{"rid":1132},{"rid":1133}],"Assets":[{"rid":1134}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1062,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-a2439cd83acd1c60c58561542cf99432/CAB-a2439cd83acd1c60c58561542cf99432","Bundle":{"rid":1033},"SubFiles":[{"rid":1135},{"rid":1136}],"Assets":[{"rid":1137}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1063,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-fbb9c1b8073b821e042a5bc47ba09cf7/CAB-fbb9c1b8073b821e042a5bc47ba09cf7","Bundle":{"rid":1034},"SubFiles":[{"rid":1138},{"rid":1139}],"Assets":[{"rid":1140}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1064,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-f6c891248d11a37872b027b369b820af/CAB-f6c891248d11a37872b027b369b820af","Bundle":{"rid":1035},"SubFiles":[{"rid":1141},{"rid":1142}],"Assets":[{"rid":1143}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1065,"type":{"class":"BuildLayout/File","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"archive:/CAB-2c2d6e9b78804ee1e800e2b1c620f98a/CAB-2c2d6e9b78804ee1e800e2b1c620f98a","Bundle":{"rid":1036},"SubFiles":[{"rid":1144},{"rid":1145}],"Assets":[{"rid":1146}],"OtherAssets":[],"ExternalReferences":[],"WriteResultFilename":"","BundleObjectInfo":{"Size":208},"PreloadInfoSize":0,"MonoScriptCount":0,"MonoScriptSize":0}},{"rid":1066,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-73b98a43ee020a5e591db9518c2cb296","IsSerializedFile":true,"Size":3680}},{"rid":1067,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-73b98a43ee020a5e591db9518c2cb296.resource","IsSerializedFile":false,"Size":19200}},{"rid":1068,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"054a1ca02d98d394b85418dd05dad9ff","AssetPath":"Assets/Samples/Saxophone1/4.mp3","InternalId":"Assets/Samples/Saxophone1/4.mp3","AssetHash":{"serializedVersion":"2","Hash":"5583cbfc81d477a4697fafa00f34c984"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"4","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":19200,"References":[]}],"MainAssetType":6,"GroupGuid":"258c7459604d085478c05930706078c2","AddressableName":"4","Labels":["AudioClip"],"SerializedSize":156,"StreamedSize":19200,"File":{"rid":1039},"Bundle":{"rid":1006},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1069,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-27752acd531d26b983a15c00264c45fb","IsSerializedFile":true,"Size":3680}},{"rid":1070,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-27752acd531d26b983a15c00264c45fb.resource","IsSerializedFile":false,"Size":30688}},{"rid":1071,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"278c261333bf8604eb5c83790d02004d","AssetPath":"Assets/Samples/Saxophone1/6.mp3","InternalId":"Assets/Samples/Saxophone1/6.mp3","AssetHash":{"serializedVersion":"2","Hash":"4b21a94723f1b72670836b8034ae42a0"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"6","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":30688,"References":[]}],"MainAssetType":6,"GroupGuid":"258c7459604d085478c05930706078c2","AddressableName":"6","Labels":["AudioClip"],"SerializedSize":156,"StreamedSize":30688,"File":{"rid":1040},"Bundle":{"rid":1007},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1072,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-1f5bcdc7a5409950f693976900dd4aa2","IsSerializedFile":true,"Size":3676}},{"rid":1073,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-1f5bcdc7a5409950f693976900dd4aa2.resource","IsSerializedFile":false,"Size":22080}},{"rid":1074,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"43b17a5450ece6b43a6b459b5eeaf648","AssetPath":"Assets/Samples/Saxophone1/3.mp3","InternalId":"Assets/Samples/Saxophone1/3.mp3","AssetHash":{"serializedVersion":"2","Hash":"fd3c09c06b08e3de6829d657a581f45c"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"3","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":22080,"References":[]}],"MainAssetType":6,"GroupGuid":"258c7459604d085478c05930706078c2","AddressableName":"3","Labels":[],"SerializedSize":156,"StreamedSize":22080,"File":{"rid":1041},"Bundle":{"rid":1008},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1075,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-0fa1a01f0e1b970a85c89e575595f749","IsSerializedFile":true,"Size":3676}},{"rid":1076,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-0fa1a01f0e1b970a85c89e575595f749.resource","IsSerializedFile":false,"Size":18496}},{"rid":1077,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"870edf9b56842b44f8f5e92f0ea5a76e","AssetPath":"Assets/Samples/Saxophone1/5.mp3","InternalId":"Assets/Samples/Saxophone1/5.mp3","AssetHash":{"serializedVersion":"2","Hash":"cf947973463a1e4e2df69fc10197fb4d"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"5","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":18496,"References":[]}],"MainAssetType":6,"GroupGuid":"258c7459604d085478c05930706078c2","AddressableName":"5","Labels":[],"SerializedSize":156,"StreamedSize":18496,"File":{"rid":1042},"Bundle":{"rid":1009},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1078,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-01315ab73001d0050a9e9a4454dc947f","IsSerializedFile":true,"Size":3680}},{"rid":1079,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-01315ab73001d0050a9e9a4454dc947f.resource","IsSerializedFile":false,"Size":18784}},{"rid":1080,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"9607360293b294646ac2684e8de61c4e","AssetPath":"Assets/Samples/Saxophone1/2.mp3","InternalId":"Assets/Samples/Saxophone1/2.mp3","AssetHash":{"serializedVersion":"2","Hash":"0b88ccd96e292fdb6f5b286deb1a29e9"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"2","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":18784,"References":[]}],"MainAssetType":6,"GroupGuid":"258c7459604d085478c05930706078c2","AddressableName":"2","Labels":[],"SerializedSize":156,"StreamedSize":18784,"File":{"rid":1043},"Bundle":{"rid":1010},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1081,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-33b13439bdd10f67ea9ad546608069c7","IsSerializedFile":true,"Size":3676}},{"rid":1082,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-33b13439bdd10f67ea9ad546608069c7.resource","IsSerializedFile":false,"Size":14976}},{"rid":1083,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"aedaaf69c851e3d47bff4d6408971c31","AssetPath":"Assets/Samples/Saxophone1/7.mp3","InternalId":"Assets/Samples/Saxophone1/7.mp3","AssetHash":{"serializedVersion":"2","Hash":"308e89a7fc814e86b6a4ddeffb537aea"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"7","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":14976,"References":[]}],"MainAssetType":6,"GroupGuid":"258c7459604d085478c05930706078c2","AddressableName":"7","Labels":[],"SerializedSize":156,"StreamedSize":14976,"File":{"rid":1044},"Bundle":{"rid":1011},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1084,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-70392fbb89818a9b1dc85933205fc393","IsSerializedFile":true,"Size":3676}},{"rid":1085,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-70392fbb89818a9b1dc85933205fc393.resource","IsSerializedFile":false,"Size":29984}},{"rid":1086,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"bb0e3bc1035198b4ba3589f09da21214","AssetPath":"Assets/Samples/Saxophone1/0.mp3","InternalId":"Assets/Samples/Saxophone1/0.mp3","AssetHash":{"serializedVersion":"2","Hash":"4005ae0b3feb4602beb3076b410a4f0d"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"0","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":29984,"References":[]}],"MainAssetType":6,"GroupGuid":"258c7459604d085478c05930706078c2","AddressableName":"0","Labels":[],"SerializedSize":156,"StreamedSize":29984,"File":{"rid":1045},"Bundle":{"rid":1012},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1087,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-0779c46a926a678e417643586b001645","IsSerializedFile":true,"Size":3680}},{"rid":1088,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-0779c46a926a678e417643586b001645.resource","IsSerializedFile":false,"Size":11424}},{"rid":1089,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"c09ca9d2c17ea134eac56c494c04a1b8","AssetPath":"Assets/Samples/Saxophone1/8.mp3","InternalId":"Assets/Samples/Saxophone1/8.mp3","AssetHash":{"serializedVersion":"2","Hash":"6c77f2c27e4bee363ba77476f17f5f14"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"8","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":11424,"References":[]}],"MainAssetType":6,"GroupGuid":"258c7459604d085478c05930706078c2","AddressableName":"8","Labels":[],"SerializedSize":156,"StreamedSize":11424,"File":{"rid":1046},"Bundle":{"rid":1013},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1090,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-0a2912740c976ca7a7b1ccc28e6e7a7a","IsSerializedFile":true,"Size":3680}},{"rid":1091,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-0a2912740c976ca7a7b1ccc28e6e7a7a.resource","IsSerializedFile":false,"Size":17600}},{"rid":1092,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"d0c2f86c1157074499c565080193d5a5","AssetPath":"Assets/Samples/Saxophone1/1.mp3","InternalId":"Assets/Samples/Saxophone1/1.mp3","AssetHash":{"serializedVersion":"2","Hash":"fa8a03b7ce0e720d3fafdae3bdc01b7d"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"1","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":17600,"References":[]}],"MainAssetType":6,"GroupGuid":"258c7459604d085478c05930706078c2","AddressableName":"1","Labels":[],"SerializedSize":156,"StreamedSize":17600,"File":{"rid":1047},"Bundle":{"rid":1014},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1093,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-05d1c88fa8f8866205a8423046199e1e","IsSerializedFile":true,"Size":3676}},{"rid":1094,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-05d1c88fa8f8866205a8423046199e1e.resource","IsSerializedFile":false,"Size":15168}},{"rid":1095,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"f778ae0076321f44ab2c21fcaac7036e","AssetPath":"Assets/Samples/Saxophone1/9.mp3","InternalId":"Assets/Samples/Saxophone1/9.mp3","AssetHash":{"serializedVersion":"2","Hash":"044e3a1ad7bb939b1477a6fb6cf454ea"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"9","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":15168,"References":[]}],"MainAssetType":6,"GroupGuid":"258c7459604d085478c05930706078c2","AddressableName":"9","Labels":[],"SerializedSize":156,"StreamedSize":15168,"File":{"rid":1048},"Bundle":{"rid":1015},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1096,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-d021e216fd6d23c0d84b2c10d7d2fe9b","IsSerializedFile":true,"Size":3684}},{"rid":1097,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-d021e216fd6d23c0d84b2c10d7d2fe9b.resource","IsSerializedFile":false,"Size":10016}},{"rid":1098,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"05a9426e7d9e40c4987c3626716fca50","AssetPath":"Assets/Samples/Generated/drum1.mp3","InternalId":"Assets/Samples/Generated/drum1.mp3","AssetHash":{"serializedVersion":"2","Hash":"d0da3f43e8ea8ffdb231525891abd7ef"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"drum1","ComponentName":"","AssetType":6,"SerializedSize":160,"StreamedSize":10016,"References":[]}],"MainAssetType":6,"GroupGuid":"a201e34a5a3fa024080d99bcfceca817","AddressableName":"drum1","Labels":[],"SerializedSize":160,"StreamedSize":10016,"File":{"rid":1049},"Bundle":{"rid":1018},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1099,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-e02a12723316bd57c4529362df4c1c7b","IsSerializedFile":true,"Size":3688}},{"rid":1100,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-e02a12723316bd57c4529362df4c1c7b.resource","IsSerializedFile":false,"Size":585312}},{"rid":1101,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"3cda7623f7b5b9246a6061ca8229b092","AssetPath":"Assets/Samples/Generated/brownian.mp3","InternalId":"Assets/Samples/Generated/brownian.mp3","AssetHash":{"serializedVersion":"2","Hash":"8fe0a5dc26e0964f26fddc597b7d7c33"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"brownian","ComponentName":"","AssetType":6,"SerializedSize":160,"StreamedSize":585312,"References":[]}],"MainAssetType":6,"GroupGuid":"a201e34a5a3fa024080d99bcfceca817","AddressableName":"brownian","Labels":[],"SerializedSize":160,"StreamedSize":585312,"File":{"rid":1050},"Bundle":{"rid":1019},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1102,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-dfc27c99e8d3e8200d1e73d09ba342fc","IsSerializedFile":true,"Size":3696}},{"rid":1103,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-dfc27c99e8d3e8200d1e73d09ba342fc.resource","IsSerializedFile":false,"Size":5600}},{"rid":1104,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"53b75bd55183b2a45a1df4c8b8165a45","AssetPath":"Assets/Samples/Generated/sine440.mp3","InternalId":"Assets/Samples/Generated/sine440.mp3","AssetHash":{"serializedVersion":"2","Hash":"c1c1e2e7e5daff3912b3a63d6c9ea6d3"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"sine440","ComponentName":"","AssetType":6,"SerializedSize":160,"StreamedSize":5600,"References":[]}],"MainAssetType":6,"GroupGuid":"a201e34a5a3fa024080d99bcfceca817","AddressableName":"sine440","Labels":[],"SerializedSize":160,"StreamedSize":5600,"File":{"rid":1051},"Bundle":{"rid":1020},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1105,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-d8c2508dc18ced397b0b7ea3e293046b","IsSerializedFile":true,"Size":3692}},{"rid":1106,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-d8c2508dc18ced397b0b7ea3e293046b.resource","IsSerializedFile":false,"Size":76512}},{"rid":1107,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"7b71f8948f4fd38428df1a68d3f125d3","AssetPath":"Assets/Samples/Generated/sine.mp3","InternalId":"Assets/Samples/Generated/sine.mp3","AssetHash":{"serializedVersion":"2","Hash":"4be37db5520820e1da7608952615032a"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"sine","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":76512,"References":[]}],"MainAssetType":6,"GroupGuid":"a201e34a5a3fa024080d99bcfceca817","AddressableName":"sine","Labels":[],"SerializedSize":156,"StreamedSize":76512,"File":{"rid":1052},"Bundle":{"rid":1021},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1108,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-ab13095ed3bae6812ebbdefdf8c8fb30","IsSerializedFile":true,"Size":3684}},{"rid":1109,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-ab13095ed3bae6812ebbdefdf8c8fb30.resource","IsSerializedFile":false,"Size":411584}},{"rid":1110,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"b5ac7dcf38759374487178c81bfb5e08","AssetPath":"Assets/Samples/Generated/square.mp3","InternalId":"Assets/Samples/Generated/square.mp3","AssetHash":{"serializedVersion":"2","Hash":"f0b63a4867fee36ca0c25fffa6e73f91"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"square","ComponentName":"","AssetType":6,"SerializedSize":160,"StreamedSize":411584,"References":[]}],"MainAssetType":6,"GroupGuid":"a201e34a5a3fa024080d99bcfceca817","AddressableName":"square","Labels":[],"SerializedSize":160,"StreamedSize":411584,"File":{"rid":1053},"Bundle":{"rid":1022},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1111,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-6d55f28146ef170678a8f3a3057434a1","IsSerializedFile":true,"Size":3704}},{"rid":1112,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-6d55f28146ef170678a8f3a3057434a1.resource","IsSerializedFile":false,"Size":50848}},{"rid":1113,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"c2dc13a18b5d4aa45a78f010cf27e471","AssetPath":"Assets/Samples/Generated/square300.mp3","InternalId":"Assets/Samples/Generated/square300.mp3","AssetHash":{"serializedVersion":"2","Hash":"20e643e7899e9d8dc0a995d60373d1d1"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"square300","ComponentName":"","AssetType":6,"SerializedSize":164,"StreamedSize":50848,"References":[]}],"MainAssetType":6,"GroupGuid":"a201e34a5a3fa024080d99bcfceca817","AddressableName":"square300","Labels":[],"SerializedSize":164,"StreamedSize":50848,"File":{"rid":1054},"Bundle":{"rid":1023},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1114,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-2d0e0b283a1fb1a45b272552509552ea","IsSerializedFile":true,"Size":3684}},{"rid":1115,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-2d0e0b283a1fb1a45b272552509552ea.resource","IsSerializedFile":false,"Size":10176}},{"rid":1116,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"d9cbbbe6d1a30f248a799b211d6fbddc","AssetPath":"Assets/Samples/Generated/drum2.mp3","InternalId":"Assets/Samples/Generated/drum2.mp3","AssetHash":{"serializedVersion":"2","Hash":"aa796e94d85494f1022d199420eb00e9"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"drum2","ComponentName":"","AssetType":6,"SerializedSize":160,"StreamedSize":10176,"References":[]}],"MainAssetType":6,"GroupGuid":"a201e34a5a3fa024080d99bcfceca817","AddressableName":"drum2","Labels":[],"SerializedSize":160,"StreamedSize":10176,"File":{"rid":1055},"Bundle":{"rid":1024},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1117,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-2a6b5cdb2a206a3d4aa0eab375f1e5d6","IsSerializedFile":true,"Size":3676}},{"rid":1118,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-2a6b5cdb2a206a3d4aa0eab375f1e5d6.resource","IsSerializedFile":false,"Size":69536}},{"rid":1119,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"196f8c9e3e9cc764db55da751c1b5d9d","AssetPath":"Assets/Samples/Saxophone2/e.mp3","InternalId":"Assets/Samples/Saxophone2/e.mp3","AssetHash":{"serializedVersion":"2","Hash":"595975d07d7c55461eaefd0cb68f39ab"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"e","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":69536,"References":[]}],"MainAssetType":6,"GroupGuid":"a56c5fee201ac7840a927f1c63a68728","AddressableName":"e","Labels":[],"SerializedSize":156,"StreamedSize":69536,"File":{"rid":1056},"Bundle":{"rid":1027},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1120,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-a209ca6fc6e41ac9f6d6fc1d57dc96e3","IsSerializedFile":true,"Size":3676}},{"rid":1121,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-a209ca6fc6e41ac9f6d6fc1d57dc96e3.resource","IsSerializedFile":false,"Size":17280}},{"rid":1122,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"3d88ab3314ac1c9438e0f35a59507386","AssetPath":"Assets/Samples/Saxophone2/c.mp3","InternalId":"Assets/Samples/Saxophone2/c.mp3","AssetHash":{"serializedVersion":"2","Hash":"186d0444728feeea6897392ad1b23646"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"c","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":17280,"References":[]}],"MainAssetType":6,"GroupGuid":"a56c5fee201ac7840a927f1c63a68728","AddressableName":"c","Labels":[],"SerializedSize":156,"StreamedSize":17280,"File":{"rid":1057},"Bundle":{"rid":1028},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1123,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-5319be78ac1d58d02d94d24bec324ca6","IsSerializedFile":true,"Size":3676}},{"rid":1124,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-5319be78ac1d58d02d94d24bec324ca6.resource","IsSerializedFile":false,"Size":59136}},{"rid":1125,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"4016dace81140b54ea0671c1720a43e3","AssetPath":"Assets/Samples/Saxophone2/j.mp3","InternalId":"Assets/Samples/Saxophone2/j.mp3","AssetHash":{"serializedVersion":"2","Hash":"d560bc884e64301ded4ab20f0c6eacb0"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"j","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":59136,"References":[]}],"MainAssetType":6,"GroupGuid":"a56c5fee201ac7840a927f1c63a68728","AddressableName":"j","Labels":[],"SerializedSize":156,"StreamedSize":59136,"File":{"rid":1058},"Bundle":{"rid":1029},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1126,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-8cc6bc79beeec25a3b2e09484debb7bf","IsSerializedFile":true,"Size":3680}},{"rid":1127,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-8cc6bc79beeec25a3b2e09484debb7bf.resource","IsSerializedFile":false,"Size":67136}},{"rid":1128,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"7939bd4f5f2fbdf49bafc5670ab6effa","AssetPath":"Assets/Samples/Saxophone2/h.mp3","InternalId":"Assets/Samples/Saxophone2/h.mp3","AssetHash":{"serializedVersion":"2","Hash":"1819624ce488110ca05f21f6f0d20afd"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"h","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":67136,"References":[]}],"MainAssetType":6,"GroupGuid":"a56c5fee201ac7840a927f1c63a68728","AddressableName":"h","Labels":[],"SerializedSize":156,"StreamedSize":67136,"File":{"rid":1059},"Bundle":{"rid":1030},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1129,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-e74367794c024c21ece7e1aa44e43744","IsSerializedFile":true,"Size":3676}},{"rid":1130,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-e74367794c024c21ece7e1aa44e43744.resource","IsSerializedFile":false,"Size":50688}},{"rid":1131,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"88339cf287767ec418310d09f36890d7","AssetPath":"Assets/Samples/Saxophone2/f.mp3","InternalId":"Assets/Samples/Saxophone2/f.mp3","AssetHash":{"serializedVersion":"2","Hash":"021fef13a6a5c26bb478c97d61593d50"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"f","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":50688,"References":[]}],"MainAssetType":6,"GroupGuid":"a56c5fee201ac7840a927f1c63a68728","AddressableName":"f","Labels":[],"SerializedSize":156,"StreamedSize":50688,"File":{"rid":1060},"Bundle":{"rid":1031},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1132,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-84aa7474c89cce2aa8ded8d7683cb357","IsSerializedFile":true,"Size":3680}},{"rid":1133,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-84aa7474c89cce2aa8ded8d7683cb357.resource","IsSerializedFile":false,"Size":19648}},{"rid":1134,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"8fb0886c8a099dd41b90c45f7661a985","AssetPath":"Assets/Samples/Saxophone2/b.mp3","InternalId":"Assets/Samples/Saxophone2/b.mp3","AssetHash":{"serializedVersion":"2","Hash":"8cf6772ae65b8cdd0f61b49192040aab"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"b","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":19648,"References":[]}],"MainAssetType":6,"GroupGuid":"a56c5fee201ac7840a927f1c63a68728","AddressableName":"b","Labels":[],"SerializedSize":156,"StreamedSize":19648,"File":{"rid":1061},"Bundle":{"rid":1032},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1135,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-a2439cd83acd1c60c58561542cf99432","IsSerializedFile":true,"Size":3676}},{"rid":1136,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-a2439cd83acd1c60c58561542cf99432.resource","IsSerializedFile":false,"Size":130624}},{"rid":1137,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"a07c3f4745fae9045a615773fd208e27","AssetPath":"Assets/Samples/Saxophone2/d.mp3","InternalId":"Assets/Samples/Saxophone2/d.mp3","AssetHash":{"serializedVersion":"2","Hash":"2fbfe1f4d362be7e6f7c1ba931ec44e6"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"d","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":130624,"References":[]}],"MainAssetType":6,"GroupGuid":"a56c5fee201ac7840a927f1c63a68728","AddressableName":"d","Labels":[],"SerializedSize":156,"StreamedSize":130624,"File":{"rid":1062},"Bundle":{"rid":1033},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1138,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-fbb9c1b8073b821e042a5bc47ba09cf7","IsSerializedFile":true,"Size":3680}},{"rid":1139,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-fbb9c1b8073b821e042a5bc47ba09cf7.resource","IsSerializedFile":false,"Size":47424}},{"rid":1140,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"b65a7916245593b4e89f4bd0aa920533","AssetPath":"Assets/Samples/Saxophone2/a.mp3","InternalId":"Assets/Samples/Saxophone2/a.mp3","AssetHash":{"serializedVersion":"2","Hash":"ef69479f5ac11a5908709f854667429a"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"a","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":47424,"References":[]}],"MainAssetType":6,"GroupGuid":"a56c5fee201ac7840a927f1c63a68728","AddressableName":"a","Labels":[],"SerializedSize":156,"StreamedSize":47424,"File":{"rid":1063},"Bundle":{"rid":1034},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1141,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-f6c891248d11a37872b027b369b820af","IsSerializedFile":true,"Size":3680}},{"rid":1142,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-f6c891248d11a37872b027b369b820af.resource","IsSerializedFile":false,"Size":75616}},{"rid":1143,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"bc3e0ce7464020d4f8a320e0b4f1fb2a","AssetPath":"Assets/Samples/Saxophone2/i.mp3","InternalId":"Assets/Samples/Saxophone2/i.mp3","AssetHash":{"serializedVersion":"2","Hash":"39e237b4cf5a5dcabf21590d8f8c31ce"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"i","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":75616,"References":[]}],"MainAssetType":6,"GroupGuid":"a56c5fee201ac7840a927f1c63a68728","AddressableName":"i","Labels":[],"SerializedSize":156,"StreamedSize":75616,"File":{"rid":1064},"Bundle":{"rid":1035},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}},{"rid":1144,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-2c2d6e9b78804ee1e800e2b1c620f98a","IsSerializedFile":true,"Size":3680}},{"rid":1145,"type":{"class":"BuildLayout/SubFile","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Name":"CAB-2c2d6e9b78804ee1e800e2b1c620f98a.resource","IsSerializedFile":false,"Size":61248}},{"rid":1146,"type":{"class":"BuildLayout/ExplicitAsset","ns":"UnityEditor.AddressableAssets.Build.Layout","asm":"Unity.Addressables.Editor"},"data":{"Guid":"dd6d9bdc76d97ad45ab51e7706ca1fe6","AssetPath":"Assets/Samples/Saxophone2/g.mp3","InternalId":"Assets/Samples/Saxophone2/g.mp3","AssetHash":{"serializedVersion":"2","Hash":"bc56a2fe480c324ea70660c567d7efd2"},"Objects":[{"LocalIdentifierInFile":8300000,"ObjectName":"g","ComponentName":"","AssetType":6,"SerializedSize":156,"StreamedSize":61248,"References":[]}],"MainAssetType":6,"GroupGuid":"a56c5fee201ac7840a927f1c63a68728","AddressableName":"g","Labels":[],"SerializedSize":156,"StreamedSize":61248,"File":{"rid":1065},"Bundle":{"rid":1036},"InternalReferencedOtherAssets":[],"InternalReferencedExplicitAssets":[],"ExternallyReferencedAssets":[],"ReferencingAssets":[]}}]}} \ No newline at end of file diff --git a/TestCommon/Data/BuildReports/AssetBundle.buildreport b/TestCommon/Data/BuildReports/AssetBundle.buildreport new file mode 100644 index 0000000..ab29721 Binary files /dev/null and b/TestCommon/Data/BuildReports/AssetBundle.buildreport differ 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/TestCommon/Data/WebBundles/HelloWorld.data b/TestCommon/Data/WebBundles/HelloWorld.data new file mode 100644 index 0000000..b9bbaa3 Binary files /dev/null and b/TestCommon/Data/WebBundles/HelloWorld.data differ diff --git a/TestCommon/Data/WebBundles/HelloWorld.data.br b/TestCommon/Data/WebBundles/HelloWorld.data.br new file mode 100644 index 0000000..a984324 Binary files /dev/null and b/TestCommon/Data/WebBundles/HelloWorld.data.br differ diff --git a/TestCommon/Data/WebBundles/HelloWorld.data.gz b/TestCommon/Data/WebBundles/HelloWorld.data.gz new file mode 100644 index 0000000..fd2657b Binary files /dev/null and b/TestCommon/Data/WebBundles/HelloWorld.data.gz differ diff --git a/TestCommon/Data/WebBundles/NotAWebBundle.txt b/TestCommon/Data/WebBundles/NotAWebBundle.txt new file mode 100644 index 0000000..4696a60 --- /dev/null +++ b/TestCommon/Data/WebBundles/NotAWebBundle.txt @@ -0,0 +1 @@ +This is NOT a web bundle. diff --git a/TestCommon/ExpectedData.cs b/TestCommon/ExpectedData.cs index 89701b2..92f2a15 100644 --- a/TestCommon/ExpectedData.cs +++ b/TestCommon/ExpectedData.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using System.IO; using Newtonsoft.Json; namespace UnityDataTools.TestCommon; @@ -15,12 +17,12 @@ public object Get(string key) { return m_ExpectedValues[key]; } - + public void Save(string path) { var settings = new JsonSerializerSettings(); settings.TypeNameHandling = TypeNameHandling.All; - + File.WriteAllText(Path.Combine(path, "ExpectedValues.json"), JsonConvert.SerializeObject(m_ExpectedValues, Formatting.Indented, settings)); } @@ -32,10 +34,10 @@ public void Load(string path) { return; } - + var settings = new JsonSerializerSettings(); settings.TypeNameHandling = TypeNameHandling.All; - + m_ExpectedValues = JsonConvert.DeserializeObject>(File.ReadAllText(path), settings); } } diff --git a/TestCommon/TestCommon.csproj b/TestCommon/TestCommon.csproj index 3177c02..9791494 100644 --- a/TestCommon/TestCommon.csproj +++ b/TestCommon/TestCommon.csproj @@ -1,10 +1,19 @@ - net6.0 + net9.0 enable disable TestCommon + latest + + + + AnyCPU + + + + AnyCPU diff --git a/TestCommon/TestFixtures.cs b/TestCommon/TestFixtures.cs index 1e0c351..72e9f2c 100644 --- a/TestCommon/TestFixtures.cs +++ b/TestCommon/TestFixtures.cs @@ -1,18 +1,25 @@ +using System.Collections.Generic; +using System.IO; using NUnit.Framework; namespace UnityDataTools.TestCommon; +// Base class that facilitates iterating through sub-sub-folders +// inside the Data location. E.g. GetContexts("AssetBundles") +// finds "TestCommon/Data/AssetBundles/2019.4.0f1", "TestCommon/Data/AssetBundles/2020.3.0f1" etc. public class BaseTestFixture { protected Context Context { get; } private static Dictionary> m_Cache = new(); - + public BaseTestFixture(Context context) { Context = context; } + // Tests that have files that record the expected results for each version + // of Unity can override this method to regenerate those expected results. protected virtual void OnLoadExpectedData(Context context) { } @@ -21,9 +28,12 @@ protected virtual void OnLoadExpectedData(Context context) public void LoadExpectedData() { OnLoadExpectedData(Context); + + // Load json file with the expected results for a test based on + // folder structure convention (e.g. ExpectedData//ExpectedVersions.json) Context.ExpectedData.Load(Context.ExpectedDataFolder); } - + protected static IEnumerable GetContexts(string dataFolder) { if (m_Cache.TryGetValue(dataFolder, out var cases)) @@ -33,9 +43,9 @@ protected static IEnumerable GetContexts(string dataFolder) cases = new List(); m_Cache[TestContext.CurrentContext.TestDirectory] = cases; - + var subfolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", dataFolder); - + foreach (var folder in Directory.EnumerateDirectories(subfolder)) { cases.Add(new Context(folder)); @@ -45,13 +55,16 @@ protected static IEnumerable GetContexts(string dataFolder) } } +// Test fixture that repeats the tests for each folder inside TestCommon/Data/AssetBundles. +// Each sub-folder is expected to have results of an AssetBundle build repeated with a +// different version of Unity. [TestFixtureSource(typeof(AssetBundleTestFixture), nameof(GetContexts))] public class AssetBundleTestFixture : BaseTestFixture { public AssetBundleTestFixture(Context context) : base(context) { } - + public static IEnumerable GetContexts() { return BaseTestFixture.GetContexts("AssetBundles"); @@ -64,7 +77,7 @@ public class PlayerDataTestFixture : BaseTestFixture public PlayerDataTestFixture(Context context) : base(context) { } - + public static IEnumerable GetContexts() { return BaseTestFixture.GetContexts("PlayerData"); diff --git a/TextDumper/README.md b/TextDumper/README.md index 497d9b4..8b801b4 100644 --- a/TextDumper/README.md +++ b/TextDumper/README.md @@ -1,58 +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 text files. - -## How to use - -The library consists of a single class called [TextDumperTool](./TextDumperTool.cs). It has a single -method named Dump and takes three 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. - -## 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 where the object is located. -* m_PathID: the object identifier in the file. 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. - -The string after the path is the SerializedFile name corresponding to the file identifier in -parenthesis. 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. +See [Documentation/textdumper.md](../Documentation/textdumper.md) diff --git a/TextDumper/TextDumper.csproj b/TextDumper/TextDumper.csproj index 9bce643..1e54dd1 100644 --- a/TextDumper/TextDumper.csproj +++ b/TextDumper/TextDumper.csproj @@ -2,9 +2,18 @@ Library - net6.0 + net9.0 + latest + + + + AnyCPU + + + + AnyCPU diff --git a/TextDumper/TextDumperTool.cs b/TextDumper/TextDumperTool.cs index 042d7e3..4846c71 100644 --- a/TextDumper/TextDumperTool.cs +++ b/TextDumper/TextDumperTool.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Text; using UnityDataTools.FileSystem; @@ -13,7 +13,7 @@ public class TextDumperTool SerializedFile m_SerializedFile; StreamWriter m_Writer; - public int Dump(string path, string outputPath, bool skipLargeArrays) + public int Dump(string path, string outputPath, bool skipLargeArrays, long objectId = 0) { m_SkipLargeArrays = skipLargeArrays; @@ -21,6 +21,8 @@ public int Dump(string path, string outputPath, bool skipLargeArrays) { try { + // Try the input as an unity archive, e.g. an AssetBundle + // In that case we dump each serialized file contained inside it. using var archive = UnityFileSystem.MountArchive(path, "/"); foreach (var node in archive.Nodes) { @@ -30,7 +32,7 @@ public int Dump(string path, string outputPath, bool skipLargeArrays) { using (m_Writer = new StreamWriter(Path.Combine(outputPath, Path.GetFileName(node.Path) + ".txt"), false)) { - OutputSerializedFile("/" + node.Path); + OutputSerializedFile("/" + node.Path, objectId); } } } @@ -40,7 +42,7 @@ public int Dump(string path, string outputPath, bool skipLargeArrays) // Try as SerializedFile using (m_Writer = new StreamWriter(Path.Combine(outputPath, Path.GetFileName(path) + ".txt"), false)) { - OutputSerializedFile(path); + OutputSerializedFile(path, objectId); } } } @@ -60,7 +62,20 @@ void RecursiveDump(TypeTreeNode node, ref long offset, int level, int arrayIndex { bool skipChildren = false; - if (!node.IsArray) + if (level > 1 && node.IsManagedReferenceRegistry) + { + // If we are already inside a ManagedReferenceRegistry, then we ignore the ManagedReferenceRegistry node + // they can appear in the TypeTrees of Managed objects, but only the root object actually has a registry + skipChildren = true; + } + else if (node.IsArray) + { + DumpArray(node, ref offset, level); + + // Skip child nodes as they were already processed here. + skipChildren = true; + } + else { m_StringBuilder.Append(' ', level * 2); @@ -106,7 +121,7 @@ void RecursiveDump(TypeTreeNode node, ref long offset, int level, int arrayIndex m_Writer.WriteLine(m_StringBuilder); m_StringBuilder.Clear(); - + if (node.IsManagedReferenceRegistry) { DumpManagedReferenceRegistry(node, ref offset, level + 1); @@ -115,13 +130,6 @@ void RecursiveDump(TypeTreeNode node, ref long offset, int level, int arrayIndex skipChildren = true; } } - else - { - DumpArray(node, ref offset, level); - - // Skip child nodes as they were already processed here. - skipChildren = true; - } if (!skipChildren) { @@ -194,7 +202,7 @@ void DumpArray(TypeTreeNode node, ref long offset, int level) else { ++level; - + for (int i = 0; i < arraySize; ++i) { RecursiveDump(dataNode, ref offset, level, i); @@ -207,14 +215,14 @@ void DumpManagedReferenceRegistry(TypeTreeNode node, ref long offset, int level) { if (node.Children.Count < 2) throw new Exception("Invalid ManagedReferenceRegistry"); - + // First child is version number. var version = m_Reader.ReadInt32(offset); RecursiveDump(node.Children[0], ref offset, level); TypeTreeNode refTypeNode; TypeTreeNode refObjData; - + if (version == 1) { // Second child is the ReferencedObject. @@ -222,11 +230,11 @@ void DumpManagedReferenceRegistry(TypeTreeNode node, ref long offset, int level) // And its children are the referenced type and data nodes. refTypeNode = refObjNode.Children[0]; refObjData = refObjNode.Children[1]; - + int i = 0; while (DumpManagedReferenceData(refTypeNode, refObjData, ref offset, level, i++)) - {} + { } } else if (version == 2) { @@ -244,7 +252,7 @@ void DumpManagedReferenceRegistry(TypeTreeNode node, ref long offset, int level) // First child is the array size. int arraySize = m_Reader.ReadInt32(offset); offset += 4; - + // Second child is the ReferencedObject. var refObjNode = refIdsArrayNode.Children[1]; @@ -253,7 +261,7 @@ void DumpManagedReferenceRegistry(TypeTreeNode node, ref long offset, int level) // First child is the rid. long rid = m_Reader.ReadInt64(offset); offset += 8; - + // And the next children are the referenced type and data nodes. refTypeNode = refObjNode.Children[1]; refObjData = refObjNode.Children[2]; @@ -262,7 +270,7 @@ void DumpManagedReferenceRegistry(TypeTreeNode node, ref long offset, int level) } else { - throw new Exception("Unsupported ManagedReferenceRegistry version"); + throw new Exception($"Unsupported ManagedReferenceRegistry version {version}"); } } @@ -270,15 +278,15 @@ bool DumpManagedReferenceData(TypeTreeNode refTypeNode, TypeTreeNode referencedT { if (refTypeNode.Children.Count < 3) throw new Exception("Invalid ReferencedManagedType"); - + m_StringBuilder.Append(' ', level * 2); m_StringBuilder.Append($"rid("); m_StringBuilder.Append(id); m_StringBuilder.Append(") ReferencedObject"); - + m_Writer.WriteLine(m_StringBuilder); m_StringBuilder.Clear(); - + ++level; var refTypeOffset = offset; @@ -286,12 +294,12 @@ bool DumpManagedReferenceData(TypeTreeNode refTypeNode, TypeTreeNode referencedT var className = m_Reader.ReadString(offset + 4, stringSize); offset += stringSize + 4; offset = (offset + 3) & ~(3); - + stringSize = m_Reader.ReadInt32(offset); var namespaceName = m_Reader.ReadString(offset + 4, stringSize); offset += stringSize + 4; offset = (offset + 3) & ~(3); - + stringSize = m_Reader.ReadInt32(offset); var assemblyName = m_Reader.ReadString(offset + 4, stringSize); offset += stringSize + 4; @@ -308,7 +316,7 @@ bool DumpManagedReferenceData(TypeTreeNode refTypeNode, TypeTreeNode referencedT m_StringBuilder.Append(' '); m_StringBuilder.Append(referencedTypeDataNode.Type); m_StringBuilder.Append(' '); - + m_Writer.WriteLine(m_StringBuilder); m_StringBuilder.Clear(); @@ -316,7 +324,7 @@ bool DumpManagedReferenceData(TypeTreeNode refTypeNode, TypeTreeNode referencedT { m_StringBuilder.Append(' ', level * 2); m_StringBuilder.Append(id == -1 ? " unknown" : " null"); - + m_Writer.WriteLine(m_StringBuilder); m_StringBuilder.Clear(); @@ -324,7 +332,7 @@ bool DumpManagedReferenceData(TypeTreeNode refTypeNode, TypeTreeNode referencedT } var refTypeRoot = m_SerializedFile.GetRefTypeTypeTreeRoot(className, namespaceName, assemblyName); - + // Dump the ReferencedObject using its own TypeTree, but skip the root. foreach (var child in refTypeRoot.Children) { @@ -334,7 +342,7 @@ bool DumpManagedReferenceData(TypeTreeNode refTypeNode, TypeTreeNode referencedT return true; } - void OutputSerializedFile(string path) + void OutputSerializedFile(string path, long objectId) { using (m_Reader = new UnityFileReader(path, 64 * 1024 * 1024)) using (m_SerializedFile = UnityFileSystem.OpenSerializedFile(path)) @@ -349,15 +357,23 @@ void OutputSerializedFile(string path) } m_Writer.WriteLine(); + bool dumpedObject = false; foreach (var obj in m_SerializedFile.Objects) { + if (objectId != 0 && obj.Id != objectId) + continue; + var root = m_SerializedFile.GetTypeTreeRoot(obj.Id); var offset = obj.Offset; m_Writer.Write($"ID: {obj.Id} (ClassID: {obj.TypeId}) "); RecursiveDump(root, ref offset, 0); m_Writer.WriteLine(); + dumpedObject = true; } + + if (objectId != 0 && !dumpedObject) + m_Writer.WriteLine($"Object with ID {objectId} not found."); } } @@ -430,4 +446,4 @@ Array ReadBasicTypeArray(TypeTreeNode node, long offset, int arraySize) return array; } } -} \ No newline at end of file +} diff --git a/UnityDataTool.Tests/AddressablesBuildLayoutTests.cs b/UnityDataTool.Tests/AddressablesBuildLayoutTests.cs new file mode 100644 index 0000000..6ad551e --- /dev/null +++ b/UnityDataTool.Tests/AddressablesBuildLayoutTests.cs @@ -0,0 +1,68 @@ +using System; +using Microsoft.Data.Sqlite; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using NUnit.Framework; + +namespace UnityDataTools.UnityDataTool.Tests; + +#pragma warning disable NUnit2005, NUnit2006 + +public class AddressablesBuildLayoutTests +{ + private string m_TestOutputFolder; + private string m_TestDataFolder; + + [OneTimeSetUp] + public void OneTimeSetup() + { + m_TestOutputFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "test_folder"); + m_TestDataFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data"); + 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)); + } + + [Test] + public async Task Analyze_BuildLayout_ContainsExpectedSQLContent() + { + // This folder contains reference files from two builds of the "AudioExample" + // Addressables test project. + // The test confirms some expected content in the database + var path = Path.Combine(m_TestDataFolder, "AddressableBuildLayouts"); + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); + + Assert.AreEqual(0, await Program.Main(new string[] { "analyze", path, "-p", "*.json" })); + using var db = SQLTestHelper.OpenDatabase(databasePath); + + // Sanity check some expected content in the output SQLite database + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM addressables_builds", 2, + "Unexpected number of builds"); + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM addressables_builds WHERE name = \"buildlayout_2025.01.28.16.35.01.json\"", 1, + "Failed to find build matching reference filename"); + SQLTestHelper.AssertQueryString(db, "SELECT unity_version FROM addressables_builds WHERE id = 1", "6000.1.0b2", + "Unexpected Unity Version"); + SQLTestHelper.AssertQueryString(db, "SELECT package_version FROM addressables_builds WHERE id = 1", "com.unity.addressables: 2.2.2", + "Unexpected Addressables version"); + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM addressables_build_bundles WHERE build_id = 1 and name = \"samplepack1_assets_0.bundle\"", 1, + "Expected to find specific AssetBundle by name"); + SQLTestHelper.AssertQueryInt(db, "SELECT file_size FROM addressables_build_bundles WHERE build_id = 2 and name = \"samplepack1_assets_0.bundle\"", 33824, + "Unexpected size for specific AssetBundle in build 2"); + SQLTestHelper.AssertQueryString(db, "SELECT packing_mode FROM addressables_build_groups WHERE build_id = 1 and name = \"SamplePack1\"", "PackSeparately", + "Unexpected packing_mode for group"); + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM asset_bundles", 0, + "Expected no AssetBundles found in reference folder"); + } +} diff --git a/UnityDataTool.Tests/BuildReportTests.cs b/UnityDataTool.Tests/BuildReportTests.cs new file mode 100644 index 0000000..7378de4 --- /dev/null +++ b/UnityDataTool.Tests/BuildReportTests.cs @@ -0,0 +1,472 @@ +using Microsoft.Data.Sqlite; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using NUnit.Framework; +using System.Collections.Generic; + +namespace UnityDataTools.UnityDataTool.Tests; + +#pragma warning disable NUnit2005, NUnit2006 + +public class BuildReportTests +{ + private string m_TestOutputFolder; + private string m_TestDataFolder; + + [OneTimeSetUp] + public void OneTimeSetup() + { + m_TestOutputFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "test_folder"); + m_TestDataFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "BuildReports"); + 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)); + } + + // Check the primary object/file tables and views which are populated by the general + // object handling of the analyzer (e.g. nothing BuildReport specific) + // This test is parameterized to run with and without "--skip-references" + // in order to show that the core object tables are not impacted by whether + // or not references are tracked. + [Test] + public async Task Analyze_BuildReport_ContainsExpected_ObjectInfo( + [Values(false, true)] bool skipReferences) + { + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); + + var args = new List { "analyze", m_TestDataFolder, "-p", "AssetBundle.buildreport" }; + if (skipReferences) + args.Add("--skip-references"); + + Assert.AreEqual(0, await Program.Main(args.ToArray())); + using var db = SQLTestHelper.OpenDatabase(databasePath); + + // Sanity check the Unity objects found in this Build report file + // Tip: The meaning of the hard coded type ids used in the queries can be found + // at https://docs.unity3d.com/6000.3/Documentation/Manual/ClassIDReference.html + + // The BuildReport object is the most important. + // PackedAssets objects are present for each output serialized file, .resS and .resource. + const int packedAssetCount = 7; + + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM objects WHERE type = 1125", 1, + "Unexpected number of BuildReport objects (type 1125)"); + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM objects WHERE type = 1126", packedAssetCount, + "Unexpected number of PackedAssets objects"); + + // This object is expected inside AssetBundle builds + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM objects WHERE type = 668709126", 1, + "Unexpected number of BuiltAssetBundleInfoSet objects"); + + // There can be other more obscure objects present, depending on the build, + // e.g. PluginBuildInfo, AudioBuildInfo, VideoBuildInfo etc. + var ttlObjCount = SQLTestHelper.QueryInt(db, "SELECT COUNT(*) FROM objects"); + Assert.That(ttlObjCount, Is.GreaterThanOrEqualTo(1+ packedAssetCount + 1), + "Unexpected number of objects in BuildReport analysis"); + + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM asset_bundles", 0, + "Expected no AssetBundles found in reference folder"); + + // + // Tests using object_view which lets us refer to objects by type name + // + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM object_view WHERE type = 'BuildReport'", 1, + "Expected exactly one BuildReport in object_view"); + + SQLTestHelper.AssertQueryString(db, "SELECT name FROM object_view WHERE type = 'BuildReport'", "Build AssetBundles", + "Unexpected name"); + + SQLTestHelper.AssertQueryString(db, "SELECT name FROM object_view WHERE type = 'BuildReport'", "Build AssetBundles", + "Unexpected BuildReport name in object_view"); + + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM object_view WHERE type = 'PackedAssets'", packedAssetCount, + "Unexpected number of PackedAssets in object_view"); + + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM object_view WHERE type = 'BuiltAssetBundleInfoSet'", 1, + "Expected exactly one BuiltAssetBundleInfoSet in object_view"); + + // Verify all rows have the same serialized_file + 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", "AssetBundle.buildreport", + "Unexpected serialized file name in object_view"); + + // Verify the BuildReport object has expected properties + var buildReportSize = SQLTestHelper.QueryInt(db, "SELECT size FROM object_view WHERE type = 'BuildReport'"); + Assert.That(buildReportSize, Is.GreaterThan(0), "BuildReport size should be greater than 0"); + + // + // Tests using view_breakdown_by_type which aggregates objects by type + // + + // Verify counts match for specific types + SQLTestHelper.AssertQueryInt(db, "SELECT count FROM view_breakdown_by_type WHERE type = 'BuildReport'", 1, + "Expected 1 BuildReport in breakdown view"); + SQLTestHelper.AssertQueryInt(db, "SELECT count FROM view_breakdown_by_type WHERE type = 'PackedAssets'", packedAssetCount, + "Expected 7 PackedAssets in breakdown view"); + + var buildReportSize2 = SQLTestHelper.QueryInt(db, "SELECT byte_size FROM view_breakdown_by_type WHERE type = 'BuildReport'"); + Assert.AreEqual(buildReportSize, buildReportSize2, "Mismatch between object_view and breakdown_view for BuildReport size"); + + // Verify pretty_size formatting exists + var buildReportPrettySize = SQLTestHelper.QueryString(db, "SELECT pretty_size FROM view_breakdown_by_type WHERE type = 'BuildReport'"); + Assert.That(buildReportPrettySize, Does.Contain("KB").Or.Contain("B"), "BuildReport pretty_size should have size unit"); + + // Verify total byte_size across all types + var totalSize = SQLTestHelper.QueryInt(db, "SELECT SUM(byte_size) FROM view_breakdown_by_type"); + Assert.That(totalSize, Is.GreaterThan(buildReportSize), + "Unexpected number of objects in BuildReport analysis"); + + // + // Tests using serialized_files table + // + 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", "AssetBundle.buildreport", + "Unexpected serialized file name"); + + // Verify asset_bundle column is empty/NULL for BuildReport files (they are not asset bundles) + var assetBundleValue = SQLTestHelper.QueryString(db, "SELECT COALESCE(asset_bundle, '') FROM serialized_files WHERE id = 0"); + Assert.That(string.IsNullOrEmpty(assetBundleValue), "BuildReport serialized file should not have asset_bundle value"); + + // Verify the serialized file name matches what we see in object_view + var serializedFileName = SQLTestHelper.QueryString(db, "SELECT name FROM serialized_files WHERE id = 0"); + var objectViewFileName = SQLTestHelper.QueryString(db, "SELECT DISTINCT serialized_file FROM object_view"); + Assert.AreEqual(serializedFileName, objectViewFileName, + "Serialized file name should match between serialized_files table and object_view"); + } + + // The BuildReport file has a simple structure with a single BuildReport object + // and all other objects referenced from its Appendicies array. + // This gives an opportunity for a detailed test that the "refs" table is properly populated. + [Test] + public async Task Analyze_BuildReport_ContainsExpectedReferences( + [Values(false, true)] bool skipReferences) + { + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); + + var args = new List { "analyze", m_TestDataFolder, "-p", "AssetBundle.buildreport" }; + if (skipReferences) + args.Add("--skip-references"); + + Assert.AreEqual(0, await Program.Main(args.ToArray())); + using var db = SQLTestHelper.OpenDatabase(databasePath); + + if (skipReferences) + { + // When --skip-references is used, the refs table should be empty + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM refs", 0, + "refs table should be empty when --skip-references is used"); + return; + } + + var buildReportId = SQLTestHelper.QueryInt(db, + "SELECT id FROM objects WHERE type = 1125"); + + var totalObjectCount = SQLTestHelper.QueryInt(db, "SELECT COUNT(*) FROM objects"); + + var expectedRefCount = totalObjectCount - 1; + SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM refs", expectedRefCount, + "BuildReport should reference all other objects"); + + SQLTestHelper.AssertQueryInt(db, $"SELECT COUNT(*) FROM refs WHERE object = {buildReportId}", expectedRefCount, + "All references should originate from BuildReport object"); + + SQLTestHelper.AssertQueryInt(db, $"SELECT COUNT(*) FROM refs WHERE referenced_object = {buildReportId}", 0, + "No object should reference the BuildReport object"); + + var refsWithWrongPath = SQLTestHelper.QueryInt(db, + "SELECT COUNT(*) FROM refs WHERE property_path NOT LIKE 'm_Appendices[%]'"); + Assert.AreEqual(0, refsWithWrongPath, "All property_path values should match pattern 'm_Appendices[N]'"); + + SQLTestHelper.AssertQueryString(db, "SELECT DISTINCT property_type FROM refs", "Object", + "All references should have property_type 'Object'"); + + var objectsNotReferenced = SQLTestHelper.QueryInt(db, + $@"SELECT COUNT(*) FROM objects + WHERE id != {buildReportId} + AND id NOT IN (SELECT referenced_object FROM refs)"); + Assert.AreEqual(0, objectsNotReferenced, + "Every object except BuildReport should be referenced exactly once"); + + var duplicateRefs = SQLTestHelper.QueryInt(db, + "SELECT COUNT(*) FROM (SELECT referenced_object, COUNT(*) as cnt FROM refs GROUP BY referenced_object HAVING cnt > 1)"); + 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 6853daf..77707c2 100644 --- a/UnityDataTool.Tests/ExpectedDataGenerator.cs +++ b/UnityDataTool.Tests/ExpectedDataGenerator.cs @@ -1,21 +1,25 @@ -using System.Data.SQLite; using System.IO; +using Microsoft.Data.Sqlite; using UnityDataTools.FileSystem; using UnityDataTools.TestCommon; namespace UnityDataTools.UnityDataTool.Tests; +// Collect and record the current output returned by the same UnityDataTool commands +// that the tests will run. Once saved these become the reference data, and if the output +// changes the tests will fail. So this can be repeated if there is an "expected" change +// in the output. public static class ExpectedDataGenerator { public static void Generate(Context context) { var expectedData = context.ExpectedData; - + UnityFileSystem.Init(); using (var archive = UnityFileSystem.MountArchive(Path.Combine(context.UnityDataFolder, "assetbundle"), "/")) { expectedData.Add("NodeCount", archive.Nodes.Count); - + foreach (var n in archive.Nodes) { expectedData.Add(n.Path + "-Size", n.Size); @@ -23,21 +27,22 @@ public static void Generate(Context context) } } UnityFileSystem.Cleanup(); - + Program.Main(new string[] { "analyze", Path.Combine(context.UnityDataFolder), "-r" }); - - using var db = new SQLiteConnection($"Data Source={Path.Combine(Directory.GetCurrentDirectory(), "database.db")};Version=3;New=True;Foreign Keys=False;"); + + using var db = new SqliteConnection($"Data Source={Path.Combine(Directory.GetCurrentDirectory(), "database.db")};Version=3;New=True;Foreign Keys=False;"); db.Open(); 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), @@ -57,30 +62,32 @@ 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; var outputFolder = Path.Combine(csprojFolder, "ExpectedData", context.UnityDataVersion); + expectedData.Save(outputFolder); + + // Also take a snapshot of the output of running "dump" commands on the test file "assetbundle" Directory.CreateDirectory(outputFolder); var dumpPath = Path.Combine(outputFolder, "dump"); Directory.CreateDirectory(dumpPath); Program.Main(new string[] { "dump", Path.Combine(context.UnityDataFolder, "assetbundle"), "-o", dumpPath }); - + dumpPath = Path.Combine(outputFolder, "dump-s"); Directory.CreateDirectory(dumpPath); Program.Main(new string[] { "dump", Path.Combine(context.UnityDataFolder, "assetbundle"), "-o", dumpPath, "-s" }); - - expectedData.Save(outputFolder); } } diff --git a/UnityDataTool.Tests/GlobalSetup.cs b/UnityDataTool.Tests/GlobalSetup.cs new file mode 100644 index 0000000..9ff332b --- /dev/null +++ b/UnityDataTool.Tests/GlobalSetup.cs @@ -0,0 +1,18 @@ +using System.Globalization; +using System.Threading; +using NUnit.Framework; + +namespace UnityDataTools.UnityDataTool.Tests; + +[SetUpFixture] +public class GlobalSetup +{ + [OneTimeSetUp] + public void RunBeforeAnyTests() + { + // Serialized test data is in en-US format. Ensure that the tests run in this culture to avoid formatting + // related false negatives. + // TODO: Fix test data to be culture agnostic. + Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US"); + } +} diff --git a/UnityDataTool.Tests/SQLTestHelper.cs b/UnityDataTool.Tests/SQLTestHelper.cs new file mode 100644 index 0000000..3f1c713 --- /dev/null +++ b/UnityDataTool.Tests/SQLTestHelper.cs @@ -0,0 +1,135 @@ +using System.IO; +using Microsoft.Data.Sqlite; +using NUnit.Framework; + +namespace UnityDataTools.UnityDataTool.Tests; + +#pragma warning disable NUnit2005, NUnit2006 + +/// +/// Helper methods for executing SQL queries against a DB created by "Analyze" +/// and validating results in tests. +/// +public static class SQLTestHelper +{ + /// + /// Default database filename used in tests. + /// + public const string DefaultDatabaseName = "database.db"; + + /// + /// Creates and opens a SQLite database connection with standard test settings. + /// + /// The path to the database file. + /// An opened SqliteConnection. Caller is responsible for disposing. + public static SqliteConnection OpenDatabase(string databasePath) + { + var db = new SqliteConnection(new SqliteConnectionStringBuilder + { + DataSource = databasePath, + Mode = SqliteOpenMode.ReadWriteCreate, + Pooling = false, + ForeignKeys = false, + }.ConnectionString); + db.Open(); + return db; + } + + /// + /// Gets the standard database path for tests (testOutputFolder/database.db). + /// + /// The test output folder path. + /// The full path to the database file. + public static string GetDatabasePath(string testOutputFolder) + { + return Path.Combine(testOutputFolder, DefaultDatabaseName); + } + + /// + /// Executes a SQL query and returns the integer result. + /// + /// The database connection to use. + /// The SQL query to execute (should return a single integer value). + /// The integer result of the query. + public static int QueryInt(SqliteConnection db, string sql) + { + using var cmd = db.CreateCommand(); + cmd.CommandText = sql; + using var reader = cmd.ExecuteReader(); + reader.Read(); + return reader.GetInt32(0); + } + + /// + /// Executes a SQL query and returns the string result. + /// + /// The database connection to use. + /// The SQL query to execute (should return a single string value). + /// The string result of the query. + public static string QueryString(SqliteConnection db, string sql) + { + using var cmd = db.CreateCommand(); + cmd.CommandText = sql; + using var reader = cmd.ExecuteReader(); + reader.Read(); + return reader.GetString(0); + } + + /// + /// Executes a SQL query and asserts the result equals the expected integer value. + /// + /// The database connection to use. + /// The SQL query to execute (should return a single integer value). + /// The expected integer result. + /// Description of what is being tested (used in assertion message). + public static void AssertQueryInt(SqliteConnection db, string sql, int expectedValue, string description) + { + using var cmd = db.CreateCommand(); + cmd.CommandText = sql; + using var reader = cmd.ExecuteReader(); + reader.Read(); + Assert.AreEqual(expectedValue, reader.GetInt32(0), description); + } + + /// + /// Executes a SQL query and asserts the result equals the expected string value. + /// + /// The database connection to use. + /// The SQL query to execute (should return a single string value). + /// The expected string result. + /// Description of what is being tested (used in assertion message). + public static void AssertQueryString(SqliteConnection db, string sql, string expectedValue, string description) + { + using var cmd = db.CreateCommand(); + cmd.CommandText = sql; + using var reader = cmd.ExecuteReader(); + 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/UnityDataTool.Tests.csproj b/UnityDataTool.Tests/UnityDataTool.Tests.csproj index 974c0aa..b96890e 100644 --- a/UnityDataTool.Tests/UnityDataTool.Tests.csproj +++ b/UnityDataTool.Tests/UnityDataTool.Tests.csproj @@ -1,9 +1,19 @@  - net6.0 + net9.0 false + + latest + + + + AnyCPU + + + + AnyCPU diff --git a/UnityDataTool.Tests/UnityDataToolTests.cs b/UnityDataTool.Tests/UnityDataToolAssetBundleTests.cs similarity index 67% rename from UnityDataTool.Tests/UnityDataToolTests.cs rename to UnityDataTool.Tests/UnityDataToolAssetBundleTests.cs index e1eb84e..b47ae73 100644 --- a/UnityDataTool.Tests/UnityDataToolTests.cs +++ b/UnityDataTool.Tests/UnityDataToolAssetBundleTests.cs @@ -1,22 +1,22 @@ using System; -using System.Data.SQLite; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Microsoft.Data.Sqlite; using NUnit.Framework; -using UnityDataTools.TestCommon; using UnityDataTools.FileSystem; +using UnityDataTools.TestCommon; namespace UnityDataTools.UnityDataTool.Tests; #pragma warning disable NUnit2005, NUnit2006 -public class UnityDataToolTests : AssetBundleTestFixture +public class UnityDataToolAssetBundleTests : AssetBundleTestFixture { private string m_TestOutputFolder; - public UnityDataToolTests(Context context) : base(context) + public UnityDataToolAssetBundleTests(Context context) : base(context) { } @@ -37,6 +37,8 @@ public void OneTimeSetup() [TearDown] public void Teardown() { + SqliteConnection.ClearAllPools(); + var testDir = new DirectoryInfo(m_TestOutputFolder); testDir.EnumerateFiles() .ToList().ForEach(f => f.Delete()); @@ -58,7 +60,7 @@ public async Task InvalidFile( } [Test] - public async Task ArchiveExtract_FilesExtractedSuccessfully( + public async Task ArchiveExtract_AssetBundle_FilesExtractedSuccessfully( [Values("", "-o archive", "--output-path archive")] string options) { var path = Path.Combine(Context.UnityDataFolder, "assetbundle"); @@ -70,32 +72,36 @@ public async Task ArchiveExtract_FilesExtractedSuccessfully( } [Test] - public async Task ArchiveList_ListFilesCorrectly() + public async Task ArchiveList_AssetBundle_ListFilesCorrectly() { var path = Path.Combine(Context.UnityDataFolder, "assetbundle"); - using var sw = new StringWriter(); - var currentOut = Console.Out; - Console.SetOut(sw); + try + { + Console.SetOut(sw); - Assert.AreEqual(0, await Program.Main(new string[] { "archive", "list", path })); + Assert.AreEqual(0, await Program.Main(new string[] { "archive", "list", path })); - var lines = sw.ToString().Split(sw.NewLine); + var lines = sw.ToString().Split(sw.NewLine); - Assert.AreEqual("CAB-5d40f7cad7c871cf2ad2af19ac542994", lines[0]); - Assert.AreEqual($" Size: {Context.ExpectedData.Get("CAB-5d40f7cad7c871cf2ad2af19ac542994-Size")}", lines[1]); - Assert.AreEqual($" Flags: {(ArchiveNodeFlags)(long)Context.ExpectedData.Get("CAB-5d40f7cad7c871cf2ad2af19ac542994-Flags")}", lines[2]); + Assert.AreEqual("CAB-5d40f7cad7c871cf2ad2af19ac542994", lines[0]); + Assert.AreEqual($" Size: {Context.ExpectedData.Get("CAB-5d40f7cad7c871cf2ad2af19ac542994-Size")}", lines[1]); + Assert.AreEqual($" Flags: {(ArchiveNodeFlags)(long)Context.ExpectedData.Get("CAB-5d40f7cad7c871cf2ad2af19ac542994-Flags")}", lines[2]); - Assert.AreEqual("CAB-5d40f7cad7c871cf2ad2af19ac542994.resS", lines[4]); - Assert.AreEqual($" Size: {Context.ExpectedData.Get("CAB-5d40f7cad7c871cf2ad2af19ac542994.resS-Size")}", lines[5]); - Assert.AreEqual($" Flags: {(ArchiveNodeFlags)(long)Context.ExpectedData.Get("CAB-5d40f7cad7c871cf2ad2af19ac542994.resS-Flags")}", lines[6]); + Assert.AreEqual("CAB-5d40f7cad7c871cf2ad2af19ac542994.resS", lines[4]); + Assert.AreEqual($" Size: {Context.ExpectedData.Get("CAB-5d40f7cad7c871cf2ad2af19ac542994.resS-Size")}", lines[5]); + Assert.AreEqual($" Flags: {(ArchiveNodeFlags)(long)Context.ExpectedData.Get("CAB-5d40f7cad7c871cf2ad2af19ac542994.resS-Flags")}", lines[6]); - Assert.AreEqual("CAB-5d40f7cad7c871cf2ad2af19ac542994.resource", lines[8]); - Assert.AreEqual($" Size: {Context.ExpectedData.Get("CAB-5d40f7cad7c871cf2ad2af19ac542994.resource-Size")}", lines[9]); - Assert.AreEqual($" Flags: {(ArchiveNodeFlags)(long)Context.ExpectedData.Get("CAB-5d40f7cad7c871cf2ad2af19ac542994.resource-Flags")}", lines[10]); + Assert.AreEqual("CAB-5d40f7cad7c871cf2ad2af19ac542994.resource", lines[8]); + Assert.AreEqual($" Size: {Context.ExpectedData.Get("CAB-5d40f7cad7c871cf2ad2af19ac542994.resource-Size")}", lines[9]); + Assert.AreEqual($" Flags: {(ArchiveNodeFlags)(long)Context.ExpectedData.Get("CAB-5d40f7cad7c871cf2ad2af19ac542994.resource-Flags")}", lines[10]); - Console.SetOut(currentOut); + } + finally + { + Console.SetOut(currentOut); + } } [Test] @@ -141,7 +147,7 @@ public async Task DumpText_SkipLargeArrays_TextFileCreatedCorrectly( [Test] public async Task Analyze_DefaultArgs_DatabaseCorrect() { - var databasePath = Path.Combine(m_TestOutputFolder, "database.db"); + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); var analyzePath = Path.Combine(Context.UnityDataFolder); Assert.AreEqual(0, await Program.Main(new string[] { "analyze", analyzePath })); @@ -153,7 +159,7 @@ public async Task Analyze_DefaultArgs_DatabaseCorrect() public async Task Analyze_WithoutRefs_DatabaseCorrect( [Values("-s", "--skip-references")] string options) { - var databasePath = Path.Combine(m_TestOutputFolder, "database.db"); + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); var analyzePath = Path.Combine(Context.UnityDataFolder); Assert.AreEqual(0, await Program.Main(new string[] { "analyze", analyzePath }.Concat(options.Split(" ")).ToArray())); @@ -165,7 +171,7 @@ public async Task Analyze_WithoutRefs_DatabaseCorrect( public async Task Analyze_WithPattern_DatabaseCorrect( [Values("-p *.", "--search-pattern *.")] string options) { - var databasePath = Path.Combine(m_TestOutputFolder, "database.db"); + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); var analyzePath = Path.Combine(Context.UnityDataFolder); Assert.AreEqual(0, await Program.Main(new string[] { "analyze", analyzePath }.Concat(options.Split(" ")).ToArray())); @@ -177,13 +183,12 @@ public async Task Analyze_WithPattern_DatabaseCorrect( public async Task Analyze_WithPatternNoMatch_DatabaseEmpty( [Values("-p *.x", "--search-pattern *.x")] string options) { - var databasePath = Path.Combine(m_TestOutputFolder, "database.db"); + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); var analyzePath = Path.Combine(Context.UnityDataFolder); Assert.AreEqual(0, await Program.Main(new string[] { "analyze", analyzePath }.Concat(options.Split(" ")).ToArray())); - using var db = new SQLiteConnection($"Data Source={databasePath};Version=3;New=True;Foreign Keys=False;"); - db.Open(); + using var db = SQLTestHelper.OpenDatabase(databasePath); using (var cmd = db.CreateCommand()) { @@ -205,10 +210,43 @@ 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 = new SQLiteConnection($"Data Source={databasePath};Version=3;New=True;Foreign Keys=False;"); - db.Open(); + using var db = SQLTestHelper.OpenDatabase(databasePath); using (var cmd = db.CreateCommand()) { @@ -250,79 +288,3 @@ private void ValidateDatabase(string databasePath, bool withRefs) } } } - -public class UnityDataToolPlayerDataTests : PlayerDataTestFixture -{ - private string m_TestOutputFolder; - - public UnityDataToolPlayerDataTests(Context context) : base(context) - { - } - - [OneTimeSetUp] - public void OneTimeSetup() - { - m_TestOutputFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "test_folder"); - Directory.CreateDirectory(m_TestOutputFolder); - Directory.SetCurrentDirectory(m_TestOutputFolder); - } - - [TearDown] - public void Teardown() - { - foreach (var file in new DirectoryInfo(m_TestOutputFolder).EnumerateFiles()) - { - file.Delete(); - } - } - - [Test] - public async Task Analyze_PlayerData_DatabaseCorrect() - { - var databasePath = Path.Combine(m_TestOutputFolder, "database.db"); - var analyzePath = Path.Combine(Context.UnityDataFolder); - - Assert.AreEqual(0, await Program.Main(new string[] { "analyze", analyzePath, "-p", "*." })); - - using var db = new SQLiteConnection($"Data Source={databasePath};Version=3;New=True;Foreign Keys=False;"); - db.Open(); - using var cmd = db.CreateCommand(); - - cmd.CommandText = - @"SELECT - (SELECT COUNT(*) FROM asset_bundles), - (SELECT COUNT(*) FROM assets), - (SELECT COUNT(*) FROM objects), - (SELECT COUNT(*) FROM refs), - (SELECT COUNT(*) FROM serialized_files)"; - - using var reader = cmd.ExecuteReader(); - - reader.Read(); - - Assert.AreEqual(0, reader.GetInt32(0)); - Assert.AreEqual(0, reader.GetInt32(1)); - Assert.Greater(reader.GetInt32(2), 0); - Assert.Greater(reader.GetInt32(3), 0); - Assert.AreEqual(1, reader.GetInt32(4)); - } - - [Test] - public async Task DumpText_PlayerData_TextFileCreatedCorrectly() - { - var path = Path.Combine(Context.UnityDataFolder, "level0"); - var outputFile = Path.Combine(m_TestOutputFolder, "level0.txt"); - - Assert.AreEqual(0, await Program.Main(new string[] { "dump", path })); - Assert.IsTrue(File.Exists(outputFile)); - - var content = File.ReadAllText(outputFile); - var expected = File.ReadAllText(Path.Combine(Context.ExpectedDataFolder, "level0.txt")); - - // Normalize line endings. - content = Regex.Replace(content, @"\r\n|\n\r|\r", "\n"); - expected = Regex.Replace(expected, @"\r\n|\n\r|\r", "\n"); - - Assert.AreEqual(expected, content); - } -} diff --git a/UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs b/UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs new file mode 100644 index 0000000..2c3cb83 --- /dev/null +++ b/UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs @@ -0,0 +1,88 @@ +using System; +using Microsoft.Data.Sqlite; +using System.IO; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using NUnit.Framework; +using UnityDataTools.TestCommon; + +namespace UnityDataTools.UnityDataTool.Tests; + +#pragma warning disable NUnit2005, NUnit2006 + +public class UnityDataToolPlayerDataTests : PlayerDataTestFixture +{ + private string m_TestOutputFolder; + + public UnityDataToolPlayerDataTests(Context context) : base(context) + { + } + + [OneTimeSetUp] + public void OneTimeSetup() + { + m_TestOutputFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "test_folder"); + Directory.CreateDirectory(m_TestOutputFolder); + Directory.SetCurrentDirectory(m_TestOutputFolder); + } + + [TearDown] + public void Teardown() + { + SqliteConnection.ClearAllPools(); + + foreach (var file in new DirectoryInfo(m_TestOutputFolder).EnumerateFiles()) + { + file.Delete(); + } + } + + [Test] + public async Task Analyze_PlayerData_DatabaseCorrect() + { + var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder); + var analyzePath = Path.Combine(Context.UnityDataFolder); + + Assert.AreEqual(0, await Program.Main(new string[] { "analyze", analyzePath, "-p", "*." })); + using var db = SQLTestHelper.OpenDatabase(databasePath); + + using var cmd = db.CreateCommand(); + + cmd.CommandText = + @"SELECT + (SELECT COUNT(*) FROM asset_bundles), + (SELECT COUNT(*) FROM assets), + (SELECT COUNT(*) FROM objects), + (SELECT COUNT(*) FROM refs), + (SELECT COUNT(*) FROM serialized_files)"; + + using var reader = cmd.ExecuteReader(); + + reader.Read(); + + Assert.AreEqual(0, reader.GetInt32(0)); + Assert.AreEqual(0, reader.GetInt32(1)); + Assert.Greater(reader.GetInt32(2), 0); + Assert.Greater(reader.GetInt32(3), 0); + Assert.AreEqual(1, reader.GetInt32(4)); + } + + [Test] + public async Task DumpText_PlayerData_TextFileCreatedCorrectly() + { + var path = Path.Combine(Context.UnityDataFolder, "level0"); + var outputFile = Path.Combine(m_TestOutputFolder, "level0.txt"); + + Assert.AreEqual(0, await Program.Main(new string[] { "dump", path })); + Assert.IsTrue(File.Exists(outputFile)); + + var content = File.ReadAllText(outputFile); + var expected = File.ReadAllText(Path.Combine(Context.ExpectedDataFolder, "level0.txt")); + + // Normalize line endings. + content = Regex.Replace(content, @"\r\n|\n\r|\r", "\n"); + expected = Regex.Replace(expected, @"\r\n|\n\r|\r", "\n"); + + Assert.AreEqual(expected, content); + } +} diff --git a/UnityDataTool.Tests/WebBundleSupportTests.cs b/UnityDataTool.Tests/WebBundleSupportTests.cs new file mode 100644 index 0000000..14b8e72 --- /dev/null +++ b/UnityDataTool.Tests/WebBundleSupportTests.cs @@ -0,0 +1,126 @@ +using System; +using Microsoft.Data.Sqlite; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using NUnit.Framework; +using UnityDataTools.FileSystem; + +namespace UnityDataTools.UnityDataTool.Tests; + +#pragma warning disable NUnit2005, NUnit2006 + +public class WebBundleSupportTests +{ + private string m_TestOutputFolder; + private string m_TestDataFolder; + + [OneTimeSetUp] + public void OneTimeSetup() + { + m_TestOutputFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "test_folder"); + m_TestDataFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "Data"); + 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)); + } + + [Test] + public void IsWebBundle_True() + { + var webBundlePath = Path.Combine(m_TestDataFolder, "WebBundles", "HelloWorld.data"); + Assert.IsTrue(Archive.IsWebBundle(new FileInfo(webBundlePath))); + } + + [Test] + public void IsWebBundle_False() + { + var nonWebBundlePath = Path.Combine(m_TestDataFolder, "WebBundles", "NotAWebBundle.txt"); + Assert.IsFalse(Archive.IsWebBundle(new FileInfo(nonWebBundlePath))); + } + + [Test] + public async Task ArchiveList_WebBundle_ListFilesCorrectly( + [Values( + "HelloWorld.data", + "HelloWorld.data.gz", + "HelloWorld.data.br" + )] string bundlePath) + { + var path = Path.Combine(m_TestDataFolder, "WebBundles", bundlePath); + using var sw = new StringWriter(); + var currentOut = Console.Out; + try + { + Console.SetOut(sw); + + Assert.AreEqual(0, await Program.Main(new string[] { "archive", "list", path })); + + var actualOutput = sw.ToString(); + + // the expectedOutput has "lf" line endings but running on Windows + // the console output will have "crlr" + actualOutput = actualOutput.Replace("\r\n", "\n"); + + var expectedOutput = ( +@"data.unity3d + Size: 253044 + +RuntimeInitializeOnLoads.json + Size: 700 + +ScriptingAssemblies.json + Size: 3060 + +boot.config + Size: 93 + +Il2CppData/Metadata/global-metadata.dat + Size: 1641180 + +Resources/unity_default_resources + Size: 607376 + +" + ); + + Assert.AreEqual(expectedOutput, actualOutput); + } + finally + { + Console.SetOut(currentOut); + } + } + + [Test] + public async Task ArchiveExtract_WebBundle_FileExtractedSuccessfully( + [Values("", "-o archive", "--output-path archive")] string options, + [Values("HelloWorld.data", "HelloWorld.data.gz", "HelloWorld.data.br")] string bundlePath) + { + var path = Path.Combine(m_TestDataFolder, "WebBundles", bundlePath); + string[] expectedFiles = { + "boot.config", + "data.unity3d", + "RuntimeInitializeOnLoads.json", + "ScriptingAssemblies.json", + Path.Combine("Il2CppData", "Metadata", "global-metadata.dat"), + Path.Combine("Resources", "unity_default_resources"), + }; + Assert.AreEqual(0, await Program.Main(new string[] { "archive", "extract", path }.Concat(options.Split(" ", StringSplitOptions.RemoveEmptyEntries)).ToArray())); + foreach (var file in expectedFiles) + { + Assert.IsTrue(File.Exists(Path.Combine(m_TestOutputFolder, "archive", file))); + } + } +} diff --git a/UnityDataTool/Archive.cs b/UnityDataTool/Archive.cs new file mode 100644 index 0000000..ba4e720 --- /dev/null +++ b/UnityDataTool/Archive.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text; +using UnityDataTools.FileSystem; + +namespace UnityDataTools.UnityDataTool; + +public static class Archive +{ + private static readonly byte[] WebBundlePrefix = Encoding.UTF8.GetBytes("UnityWebData1.0\0"); + + public static int HandleExtract(FileInfo filename, DirectoryInfo outputFolder) + { + try + { + if (IsWebBundle(filename)) + { + ExtractWebBundle(filename, outputFolder); + } + else + { + ExtractAssetBundle(filename, outputFolder); + } + } + catch (Exception err) when ( + err is NotSupportedException + || err is FileFormatException) + { + Console.Error.WriteLine("Error opening archive"); + Console.Error.WriteLine(err.Message); + return 1; + } + return 0; + } + + public static int HandleList(FileInfo filename) + { + try + { + if (IsWebBundle(filename)) + { + ListWebBundle(filename); + } + else + { + ListAssetBundle(filename); + } + } + catch (Exception err) when ( + err is NotSupportedException + || err is FileFormatException) + { + Console.Error.WriteLine("Error opening archive"); + Console.Error.WriteLine(err.Message); + return 1; + } + + return 0; + } + + + public static bool IsWebBundle(FileInfo filename) + { + var path = filename.ToString(); + return ( + path.EndsWith(".data") + || path.EndsWith(".data.gz") + || path.EndsWith(".data.br") + ); + } + + struct WebBundleFileDescription + { + public uint ByteOffset; + public uint Size; + public string Path; + } + + static void ExtractWebBundle(FileInfo filename, DirectoryInfo outputFolder) + { + Console.WriteLine($"Extracting web bundle: {filename}"); + using var fileStream = File.Open(filename.ToString(), FileMode.Open); + using var stream = GetStream(filename, fileStream); + using var reader = new BinaryReader(stream, Encoding.UTF8); + var fileDescriptions = ParseWebBundleHeader(reader); + foreach (var description in fileDescriptions) + { + ExtractFileFromWebBundle(description, reader, outputFolder); + } + } + + static Stream GetStream(FileInfo filename, FileStream fileStream) + { + var fileExtension = Path.GetExtension(filename.ToString()); + return fileExtension switch + { + ".data" => fileStream, + ".gz" => new GZipStream(fileStream, CompressionMode.Decompress), + ".br" => new BrotliStream(fileStream, CompressionMode.Decompress), + _ => throw new FileFormatException("Incorrect file extension for web bundle"), + }; + } + + static List ParseWebBundleHeader(BinaryReader reader) + { + var result = new List(); + var prefix = ReadBytes(reader, WebBundlePrefix.Length); + if (!prefix.SequenceEqual(WebBundlePrefix)) + { + throw new FileFormatException("File is not a valid web bundle."); + } + uint headerSize = ReadUInt32(reader); + // Advance offset past prefix string and header size uint. + var currentByteOffset = WebBundlePrefix.Length + sizeof(uint); + while (currentByteOffset < headerSize) + { + var fileByteOffset = ReadUInt32(reader); + var fileSize = ReadUInt32(reader); + var filePathLength = ReadUInt32(reader); + var filePath = Encoding.UTF8.GetString(ReadBytes(reader, (int)filePathLength)); + result.Add(new WebBundleFileDescription() + { + ByteOffset = fileByteOffset, + Size = fileSize, + Path = filePath, + }); + // Advance byte offset, so we keep track of the position (to know when we're done reading the header). + currentByteOffset += 3 * sizeof(uint) + filePath.Length; + } + return result; + } + + static void ExtractFileFromWebBundle(WebBundleFileDescription description, BinaryReader reader, DirectoryInfo outputFolder) + { + // This function assumes `reader` is at the start of the binary data representing the file contents. + Console.WriteLine($"... Extracting {description.Path}"); + var path = Path.Combine(outputFolder.ToString(), description.Path); + Directory.CreateDirectory(Path.GetDirectoryName(path)); + File.WriteAllBytes(path, ReadBytes(reader, (int)description.Size)); + } + + static uint ReadUInt32(BinaryReader reader) + { + try + { + return reader.ReadUInt32(); + } + catch (EndOfStreamException) + { + throw new FileFormatException("File data is corrupt."); + } + } + + static byte[] ReadBytes(BinaryReader reader, int count) + { + var result = reader.ReadBytes(count); + if (result.Length != count) + { + throw new FileFormatException("File data is corrupt."); + } + return result; + } + + static void ExtractAssetBundle(FileInfo filename, DirectoryInfo outputFolder) + { + Console.WriteLine($"Extracting asset bundle: {filename}"); + using var archive = UnityFileSystem.MountArchive(filename.FullName, "/"); + foreach (var node in archive.Nodes) + { + Console.WriteLine($"... Extracting {node.Path}"); + CopyFile("/" + node.Path, Path.Combine(outputFolder.FullName, node.Path)); + } + } + + static void ListAssetBundle(FileInfo filename) + { + using var archive = UnityFileSystem.MountArchive(filename.FullName, "/"); + foreach (var node in archive.Nodes) + { + Console.WriteLine($"{node.Path}"); + Console.WriteLine($" Size: {node.Size}"); + Console.WriteLine($" Flags: {node.Flags}"); + Console.WriteLine(); + } + } + + static void ListWebBundle(FileInfo filename) + { + using var fileStream = File.Open(filename.ToString(), FileMode.Open); + using var stream = GetStream(filename, fileStream); + using var reader = new BinaryReader(stream, Encoding.UTF8); + var fileDescriptions = ParseWebBundleHeader(reader); + foreach (var description in fileDescriptions) + { + Console.WriteLine($"{description.Path}"); + Console.WriteLine($" Size: {description.Size}"); + Console.WriteLine(); + } + } + + static void CopyFile(string source, string dest) + { + using var sourceFile = UnityFileSystem.OpenFile(source); + // Create the containing directory if it doesn't exist. + Directory.CreateDirectory(Path.GetDirectoryName(dest)); + using var destFile = new FileStream(dest, FileMode.Create); + + const int blockSize = 256 * 1024; + var buffer = new byte[blockSize]; + long actualSize; + + do + { + actualSize = sourceFile.Read(blockSize, buffer); + destFile.Write(buffer, 0, (int)actualSize); + } + while (actualSize == blockSize); + } +} diff --git a/UnityDataTool/Program.cs b/UnityDataTool/Program.cs index 47adcd5..59fae2d 100644 --- a/UnityDataTool/Program.cs +++ b/UnityDataTool/Program.cs @@ -1,14 +1,20 @@ -using System; +using System; using System.CommandLine; using System.IO; using System.Threading.Tasks; using UnityDataTools.Analyzer; +using UnityDataTools.FileSystem; using UnityDataTools.ReferenceFinder; using UnityDataTools.TextDumper; -using UnityDataTools.FileSystem; namespace UnityDataTools.UnityDataTool; +public enum OutputFormat +{ + Text, + Json +} + public static class Program { public static async Task Main(string[] args) @@ -23,6 +29,8 @@ public static async Task Main(string[] args) var sOpt = new Option(aliases: new[] { "--skip-references", "-s" }, description: "Skip CRC and do not extract references"); var rOpt = new Option(aliases: new[] { "--extract-references", "-r" }) { IsHidden = true }; var pOpt = new Option(aliases: new[] { "--search-pattern", "-p" }, description: "File search pattern", getDefaultValue: () => "*"); + var vOpt = new Option(aliases: new[] { "--verbose", "-v" }, description: "Verbose output"); + var recurseOpt = new Option(aliases: new[] { "--no-recurse" }, description: "Do not analyze contents of subdirectories inside path"); var analyzeCommand = new Command("analyze", "Analyze AssetBundles or SerializedFiles.") { @@ -31,12 +39,14 @@ public static async Task Main(string[] args) sOpt, rOpt, pOpt, + vOpt, + recurseOpt }; analyzeCommand.AddAlias("analyse"); analyzeCommand.SetHandler( - (DirectoryInfo di, string o, bool s, bool r, string p) => Task.FromResult(HandleAnalyze(di, o, s, r, p)), - pathArg, oOpt, sOpt, rOpt, pOpt); + (DirectoryInfo di, string o, bool s, bool r, string p, bool v, bool recurseOpt) => Task.FromResult(HandleAnalyze(di, o, s, r, p, v, recurseOpt)), + pathArg, oOpt, sOpt, rOpt, pOpt, vOpt, recurseOpt); rootCommand.AddCommand(analyzeCommand); } @@ -70,18 +80,20 @@ public static async Task Main(string[] args) var pathArg = new Argument("filename", "The path of the file to dump").ExistingOnly(); var fOpt = new Option(aliases: new[] { "--output-format", "-f" }, description: "Output format", getDefaultValue: () => DumpFormat.Text); var sOpt = new Option(aliases: new[] { "--skip-large-arrays", "-s" }, description: "Do not dump large arrays of basic data types"); - var oOpt = new Option(aliases: new[] { "--output-path", "-o"}, description: "Output folder", getDefaultValue: () => new DirectoryInfo(Environment.CurrentDirectory)); + var oOpt = new Option(aliases: new[] { "--output-path", "-o" }, description: "Output folder", getDefaultValue: () => new DirectoryInfo(Environment.CurrentDirectory)); + var objectIdOpt = new Option(aliases: new[] { "--objectid", "-i" }, () => 0, "Only dump the object with this signed 64-bit id (default: 0, dump all objects)"); - var dumpCommand = new Command("dump", "Dump the content of an AssetBundle or SerializedFile.") + var dumpCommand = new Command("dump", "Dump the contents of an AssetBundle or SerializedFile.") { pathArg, fOpt, sOpt, oOpt, + objectIdOpt, }; dumpCommand.SetHandler( - (FileInfo fi, DumpFormat f, bool s, DirectoryInfo o) => Task.FromResult(HandleDump(fi, f, s, o)), - pathArg, fOpt, sOpt, oOpt); + (FileInfo fi, DumpFormat f, bool s, DirectoryInfo o, long objectId) => Task.FromResult(HandleDump(fi, f, s, o, objectId)), + pathArg, fOpt, sOpt, oOpt, objectIdOpt); rootCommand.AddCommand(dumpCommand); } @@ -90,26 +102,26 @@ public static async Task Main(string[] args) var pathArg = new Argument("filename", "The path of the archive file").ExistingOnly(); var oOpt = new Option(aliases: new[] { "--output-path", "-o" }, description: "Output directory of the extracted archive", getDefaultValue: () => new DirectoryInfo("archive")); - var extractArchiveCommand = new Command("extract", "Extract the archive.") + var extractArchiveCommand = new Command("extract", "Extract an AssetBundle or .data file.") { pathArg, oOpt, }; extractArchiveCommand.SetHandler( - (FileInfo fi, DirectoryInfo o) => Task.FromResult(HandleExtractArchive(fi, o)), + (FileInfo fi, DirectoryInfo o) => Task.FromResult(Archive.HandleExtract(fi, o)), pathArg, oOpt); - var listArchiveCommand = new Command("list", "List the content of an archive.") + var listArchiveCommand = new Command("list", "List the contents of an AssetBundle or .data file.") { pathArg, }; listArchiveCommand.SetHandler( - (FileInfo fi) => Task.FromResult(HandleListArchive(fi)), + (FileInfo fi) => Task.FromResult(Archive.HandleList(fi)), pathArg); - var archiveCommand = new Command("archive", "Unity Archive (AssetBundle) functions.") + var archiveCommand = new Command("archive", "Inspect or extract the contents of a Unity archive (AssetBundle or web platform .data file).") { extractArchiveCommand, listArchiveCommand, @@ -118,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(); @@ -130,7 +177,14 @@ enum DumpFormat Text, } - static int HandleAnalyze(DirectoryInfo path, string outputFile, bool skipReferences, bool extractReferences, string searchPattern) + static int HandleAnalyze( + DirectoryInfo path, + string outputFile, + bool skipReferences, + bool extractReferences, + string searchPattern, + bool verbose, + bool noRecurse) { var analyzer = new AnalyzerTool(); @@ -139,7 +193,7 @@ static int HandleAnalyze(DirectoryInfo path, string outputFile, bool skipReferen Console.WriteLine("WARNING: --extract-references, -r option is deprecated (references are now extracted by default)"); } - return analyzer.Analyze(path.FullName, outputFile, searchPattern, skipReferences); + return analyzer.Analyze(path.FullName, outputFile, searchPattern, skipReferences, verbose, noRecurse); } static int HandleFindReferences(FileInfo databasePath, string outputFile, long? objectId, string objectName, string objectType, bool findAll) @@ -162,78 +216,17 @@ static int HandleFindReferences(FileInfo databasePath, string outputFile, long? } } - static int HandleDump(FileInfo filename, DumpFormat format, bool skipLargeArrays, DirectoryInfo outputFolder) + static int HandleDump(FileInfo filename, DumpFormat format, bool skipLargeArrays, DirectoryInfo outputFolder, long objectId = 0) { switch (format) { case DumpFormat.Text: { var textDumper = new TextDumperTool(); - return textDumper.Dump(filename.FullName, outputFolder.FullName, skipLargeArrays); + return textDumper.Dump(filename.FullName, outputFolder.FullName, skipLargeArrays, objectId); } } return 1; } - - static int HandleExtractArchive(FileInfo filename, DirectoryInfo outputFolder) - { - try - { - using var archive = UnityFileSystem.MountArchive(filename.FullName, "/"); - foreach (var node in archive.Nodes) - { - Console.WriteLine($"Extracting {node.Path}..."); - CopyFile("/" + node.Path, Path.Combine(outputFolder.FullName, node.Path)); - } - } - catch (NotSupportedException) - { - Console.Error.WriteLine("Error opening archive!"); - return 1; - } - - return 0; - } - - static int HandleListArchive(FileInfo filename) - { - try - { - using var archive = UnityFileSystem.MountArchive(filename.FullName, "/"); - foreach (var node in archive.Nodes) - { - Console.WriteLine($"{node.Path}"); - Console.WriteLine($" Size: {node.Size}"); - Console.WriteLine($" Flags: {node.Flags}"); - Console.WriteLine(); - } - } - catch (NotSupportedException) - { - Console.Error.WriteLine("Error opening archive!"); - return 1; - } - - return 0; - } - - static void CopyFile(string source, string dest) - { - using var sourceFile = UnityFileSystem.OpenFile(source); - // Create the containing directory if it doesn't exist. - Directory.CreateDirectory(Path.GetDirectoryName(dest)); - using var destFile = new FileStream(dest, FileMode.Create); - - const int blockSize = 256 * 1024; - var buffer = new byte[blockSize]; - long actualSize; - - do - { - actualSize = sourceFile.Read(blockSize, buffer); - destFile.Write(buffer, 0, (int)actualSize); - } - while (actualSize == blockSize); - } } diff --git a/UnityDataTool/README.md b/UnityDataTool/README.md index f3148ba..8a871ca 100644 --- a/UnityDataTool/README.md +++ b/UnityDataTool/README.md @@ -1,94 +1,3 @@ # UnityDataTool -The UnityDataTool is a command line tool providing a set of commands related to Unity data files. It -is the main project of this repository and produces an executable file that can be found in -UnityDataTool/bin/[target specific folders] after a build. - -> Note that on Mac, you need to publish the UnityDataTool project to get an executable file. You -can do it from your IDE or execute this command in the UnityDataTool folder (not from the root -folder): -> -> `dotnet publish -c Release -r osx-x64 -p:PublishSingleFile=true -p:UseAppHost=true` -> -> Also on Mac, if you get a warning because "UnityFileSystemApi.dylib" cannot be opened because the -developer cannot be verified, click "Cancel" and then open the System Preferences -> Security & -Privacy window. You should be able to allow the file from there. - -The tool is invoked from the command line like this: - -`UnityDataTool [command] [command options]` - -# Commands - -## analyze/analyse - -This command extracts information from AssetBundles and SerializedFiles and dumps the results -into a SQLite database. The files must include a TypeTree, otherwise it will fail. It is an -improved version of the [AssetBundle Analyzer](https://github.com/faelenor/asset-bundle-analyzer). - -The command takes the path of the folder containing the files to analyze as argument. It also -provides the following options: -* -o, --output-file \: filename of the database that will be created, the - default is database.db. -* -s, --skip-references: skip CRC and reference (PPtrs) extraction. Faster processing and smaller - database, but inaccurate duplicate asset detection and no references table. -* -p, --search-pattern \: search pattern used to determine which files are asset bundles, - the default is \*. - -Example: `UnityDataTool analyze /path/to/asset/bundles -o my_database.db -p *.ab` - -**Refer to this [documentation](../Analyzer/README.md#How-to-use-the-database) for more information -about the output database structure.** - -## find-refs - -> Note: this is an experimental command, it may not work as expected. - -This command finds reference chains leading to specific objects. It requires a database that was -created by the 'analyze' command without the --skip-references 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. It can be -particularly useful to determine why an asset was included (and potentially more than once) in a -build. - -The command takes the path of the database as argument. It also provides the following options: -* -i, --object-id \: the id of the object to analyze ('id' column in the database). -* -n, --object-name \: name of the objects to analyze (it can be useful to find the origin - of duplicates as they will have different ids but the same name). -* -t, --object-type \: type of the objects to analyze, used to filter objects when using - the -n option. -* -o, --output-file \: name of the output file. -* -a, --find-all: this will force a search for all reference chains originating from the same root. - object instead of stopping at the first one. It may take a lot more time. Note that - either --object-id or --object-name must be provided. - -Example: `UnityDataTool find-refs my_database.db -n "MyObjectName" -t "Texture2D" -o -references.txt` - -**Refer to this [documentation](../ReferenceFinder/README.md#How-to-interpret-the-output-file) for -more information about the content of the output file.** - -## dump - -This command dumps the content of a SerializedFile into a file of the selected format. It currently -only supports the 'text' format, which is similar to the binary2text output format. - -The command takes the path of the file to dump as argument. It also provides the following options: -* -f, --output-format \: output format, default is 'text'. -* -s, --skip-large-arrays: the content of basic data type arrays with a large number of elements - won't be dumped. - -Example: `UnityDataTool dump /path/to/file` - -**Refer to this [documentation](../TextDumper/README.md#How-to-interpret-the-output-files) for more -information about the content of the output file.** - -## archive - -The archive command offers a set of archive-related sub-commands. - -**extract** This sub-command extracts the content of an archive. It takes the archive path as -argument and also provides the following option: -* -o, --output-path \: Output directory of the extracted archive (default: archive) - -**list** This sub-command lists the content of an archive. It takes the archive path as argument. +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 0a63f44..4e6320e 100644 --- a/UnityDataTool/UnityDataTool.csproj +++ b/UnityDataTool/UnityDataTool.csproj @@ -2,11 +2,25 @@ Exe - net6.0 + net9.0 + latest + 1.3.0 + 1.3.0.0 + 1.3.0.0 + 1.3.0 + + + + AnyCPU + + + + AnyCPU + diff --git a/UnityFileSystem.Tests/DllWrapperTests.cs b/UnityFileSystem.Tests/DllWrapperTests.cs index 3693985..f011ad1 100644 --- a/UnityFileSystem.Tests/DllWrapperTests.cs +++ b/UnityFileSystem.Tests/DllWrapperTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Text; using NUnit.Framework; @@ -111,7 +111,7 @@ public class DllMountUnmountTests : AssetBundleTestFixture public DllMountUnmountTests(Context context) : base(context) { } - + [OneTimeSetUp] public void Setup() { @@ -429,7 +429,7 @@ public class DllSerializedFileTests : AssetBundleTestFixture public DllSerializedFileTests(Context context) : base(context) { } - + [OneTimeSetUp] public void Setup() { @@ -527,11 +527,11 @@ public void GetExternalReference_ValidSerializedFile_ReturnExpectedExternalRefer DllWrapper.GetExternalReferenceCount(file, out var count); var path = new StringBuilder(256); var guid = new StringBuilder(32); - + for (int i = 0; i < count; ++i) { var r = DllWrapper.GetExternalReference(file, i, path, 256, guid, out var refType); - + Assert.AreEqual(ReturnCode.Success, r); Assert.AreEqual(Context.ExpectedData.Get($"CAB-5d40f7cad7c871cf2ad2af19ac542994-ExtRef{i}-Guid"), guid.ToString()); Assert.AreEqual(Context.ExpectedData.Get($"CAB-5d40f7cad7c871cf2ad2af19ac542994-ExtRef{i}-Path"), path.ToString()); @@ -584,10 +584,10 @@ public void GetObjectInfo_ValidSerializedFile_ReturnExpectedObjectInfo() Assert.AreEqual(Context.ExpectedData.Get("CAB-5d40f7cad7c871cf2ad2af19ac542994-FirstObj-Size"), objectInfo[0].Size); Assert.AreEqual(Context.ExpectedData.Get("CAB-5d40f7cad7c871cf2ad2af19ac542994-FirstObj-TypeId"), objectInfo[0].TypeId); - Assert.AreEqual(Context.ExpectedData.Get("CAB-5d40f7cad7c871cf2ad2af19ac542994-LastObj-Id"), objectInfo[count-1].Id); - Assert.AreEqual(Context.ExpectedData.Get("CAB-5d40f7cad7c871cf2ad2af19ac542994-LastObj-Offset"), objectInfo[count-1].Offset); - Assert.AreEqual(Context.ExpectedData.Get("CAB-5d40f7cad7c871cf2ad2af19ac542994-LastObj-Size"), objectInfo[count-1].Size); - Assert.AreEqual(Context.ExpectedData.Get("CAB-5d40f7cad7c871cf2ad2af19ac542994-LastObj-TypeId"), objectInfo[count-1].TypeId); + Assert.AreEqual(Context.ExpectedData.Get("CAB-5d40f7cad7c871cf2ad2af19ac542994-LastObj-Id"), objectInfo[count - 1].Id); + Assert.AreEqual(Context.ExpectedData.Get("CAB-5d40f7cad7c871cf2ad2af19ac542994-LastObj-Offset"), objectInfo[count - 1].Offset); + Assert.AreEqual(Context.ExpectedData.Get("CAB-5d40f7cad7c871cf2ad2af19ac542994-LastObj-Size"), objectInfo[count - 1].Size); + Assert.AreEqual(Context.ExpectedData.Get("CAB-5d40f7cad7c871cf2ad2af19ac542994-LastObj-TypeId"), objectInfo[count - 1].TypeId); file.Dispose(); } @@ -602,14 +602,14 @@ public void GetObjectInfo_InvalidHandle_ReturnError() public class DllTypeTreeTests : AssetBundleTestFixture { - private UnityArchiveHandle m_Archive; - private SerializedFileHandle m_SerializedFile; - private ObjectInfo[] m_Objects; + private UnityArchiveHandle m_Archive; + private SerializedFileHandle m_SerializedFile; + private ObjectInfo[] m_Objects; public DllTypeTreeTests(Context context) : base(context) { } - + [OneTimeSetUp] public void Setup() { @@ -781,4 +781,4 @@ public void GetTypeTreeNodeInfo_RefTypeTypeTree_ReturnExpectedValues() Assert.AreEqual("string", type.ToString()); Assert.AreEqual("m_Description", name.ToString()); } -} \ No newline at end of file +} diff --git a/UnityFileSystem.Tests/ExpectedDataGenerator.cs b/UnityFileSystem.Tests/ExpectedDataGenerator.cs index 38796e3..f183e7a 100644 --- a/UnityFileSystem.Tests/ExpectedDataGenerator.cs +++ b/UnityFileSystem.Tests/ExpectedDataGenerator.cs @@ -60,12 +60,12 @@ public static void Generate(Context context) } UnityFileSystem.Cleanup(); - + var csprojFolder = Directory.GetParent(context.TestDataFolder).Parent.Parent.Parent.FullName; var outputFolder = Path.Combine(csprojFolder, "ExpectedData", context.UnityDataVersion); Directory.CreateDirectory(outputFolder); - + expectedData.Save(outputFolder); } } diff --git a/UnityFileSystem.Tests/UnityFileSystem.Tests.csproj b/UnityFileSystem.Tests/UnityFileSystem.Tests.csproj index 8be00a7..074bc9c 100644 --- a/UnityFileSystem.Tests/UnityFileSystem.Tests.csproj +++ b/UnityFileSystem.Tests/UnityFileSystem.Tests.csproj @@ -1,11 +1,17 @@  - net6.0 - + net9.0 false + latest + + + + AnyCPU + - AnyCPU;x86 + + AnyCPU diff --git a/UnityFileSystem.Tests/UnityFileSystemTests.cs b/UnityFileSystem.Tests/UnityFileSystemTests.cs index 900a43e..47fd784 100644 --- a/UnityFileSystem.Tests/UnityFileSystemTests.cs +++ b/UnityFileSystem.Tests/UnityFileSystemTests.cs @@ -16,19 +16,19 @@ public class ArchiveTests : AssetBundleTestFixture public ArchiveTests(Context context) : base(context) { } - + protected override void OnLoadExpectedData(Context context) { // Uncomment to regenerate expected data. //ExpectedDataGenerator.Generate(context); } - + [OneTimeSetUp] public void Setup() { UnityFileSystem.Init(); } - + [OneTimeTearDown] public void TearDown() { @@ -50,7 +50,7 @@ public void MountArchive_InvalidArchive_ThrowsException() var ex = Assert.Throws(() => UnityFileSystem.MountArchive(path, "archive:/")); Assert.AreEqual($"Invalid file format reading {path}.", ex.Message); } - + public void MountArchive_ValidArchive_ReturnsArchive() { var path = Path.Combine(Context.UnityDataFolder, "assetbundle"); @@ -61,7 +61,7 @@ public void MountArchive_ValidArchive_ReturnsArchive() archive.Dispose(); } - + [Ignore("This test doesn't return the expected error, this condition is probably not handled correctly in Unity")] public void DisposeArchive_ValidArchive_UnmountsArchive() { @@ -74,16 +74,16 @@ public void DisposeArchive_ValidArchive_UnmountsArchive() archive.Dispose(); } - + public void Nodes_Disposed_ThrowsException() { var path = Path.Combine(Context.UnityDataFolder, "assetbundle"); var archive = UnityFileSystem.MountArchive(path, "archive:/"); archive.Dispose(); - + Assert.Throws(() => { var _ = archive.Nodes; }); } - + public void Nodes_ValidArchive_ExpectedContent(string testFolder) { var path = Path.Combine(testFolder, "AssetBundles", "assetbundle"); @@ -121,7 +121,7 @@ public UnityFileTests(Context context) : base(context) public void Setup() { UnityFileSystem.Init(); - + var path = Path.Combine(Context.UnityDataFolder, "assetbundle"); m_Archive = UnityFileSystem.MountArchive(path, "archive:/"); } @@ -130,7 +130,7 @@ public void Setup() public void TearDown() { m_Archive.Dispose(); - + UnityFileSystem.Cleanup(); } @@ -282,7 +282,7 @@ public SerializedFileTests(Context context) : base(context) public void Setup() { UnityFileSystem.Init(); - + var path = Path.Combine(Context.UnityDataFolder, "assetbundle"); m_Archive = UnityFileSystem.MountArchive(path, "archive:/"); } @@ -291,7 +291,7 @@ public void Setup() public void TearDown() { m_Archive.Dispose(); - + UnityFileSystem.Cleanup(); } @@ -314,7 +314,7 @@ public void OpenSerializedFile_NotSerializedFile_ThrowsException() public void OpenSerializedFile_ValidSerializedFile_ReturnsFile() { SerializedFile file = null; - + Assert.DoesNotThrow(() => file = UnityFileSystem.OpenSerializedFile("archive:/CAB-5d40f7cad7c871cf2ad2af19ac542994")); Assert.IsNotNull(file); @@ -419,7 +419,7 @@ public TypeTreeTests(Context context) : base(context) public void Setup() { UnityFileSystem.Init(); - + var path = Path.Combine(Context.UnityDataFolder, "assetbundle"); m_Archive = UnityFileSystem.MountArchive(path, "archive:/"); @@ -431,7 +431,7 @@ public void TearDown() { m_SerializedFile.Dispose(); m_Archive.Dispose(); - + UnityFileSystem.Cleanup(); } @@ -509,7 +509,7 @@ public void GetRefTypeTypeTree_InvalidFQN_ThrowsException() public void GetRefTypeTree_ValidSerializedFile_ReturnNode() { TypeTreeNode node = null; - + Assert.DoesNotThrow(() => node = m_SerializedFile.GetRefTypeTypeTreeRoot("SerializeReferencePolymorphismExample/Apple", "", "Assembly-CSharp")); Assert.NotNull(node); } @@ -523,11 +523,11 @@ public void GetTypeTreeNodeInfo_RefTypeTypeTree_ReturnExpectedValues() Assert.AreEqual(2, node.Children.Count); Assert.AreEqual("Apple", node.Type); Assert.AreEqual("Base", node.Name); - + Assert.AreEqual("int", node.Children[0].Type); Assert.AreEqual("m_Data", node.Children[0].Name); Assert.AreEqual(4, node.Children[0].Size); - + Assert.AreEqual("string", node.Children[1].Type); Assert.AreEqual("m_Description", node.Children[1].Name); } @@ -552,7 +552,7 @@ public void Setup() m_Archive = UnityFileSystem.MountArchive(path, "archive:/"); m_SerializedFile = UnityFileSystem.OpenSerializedFile("archive:/CAB-5d40f7cad7c871cf2ad2af19ac542994"); - m_Reader = new UnityFileReader("archive:/CAB-5d40f7cad7c871cf2ad2af19ac542994", 1024*1024); + m_Reader = new UnityFileReader("archive:/CAB-5d40f7cad7c871cf2ad2af19ac542994", 1024 * 1024); } [OneTimeTearDown] @@ -573,13 +573,13 @@ ObjectInfo GetObjectInfo(long id) for (i = 0; i < m_SerializedFile.Objects.Count; ++i) { obj = m_SerializedFile.Objects[i]; - + if (obj.Id == id) { break; } } - + Assert.Less(i, m_SerializedFile.Objects.Count); return obj; @@ -589,31 +589,31 @@ ObjectInfo GetObjectInfo(long id) public void AccessProperty_ValidProperty_ReturnExpectedValues() { var obj = GetObjectInfo(-7865028809519950684); - + var root = m_SerializedFile.GetTypeTreeRoot(obj.Id); var reader = new TypeTreeReaders.RandomAccessReader(m_SerializedFile, root, m_Reader, obj.Offset); - + Assert.AreEqual("Lame", reader["m_Name"].GetValue()); Assert.AreEqual(228, reader["m_SubMeshes"][0]["vertexCount"].GetValue()); Assert.AreEqual(false, reader["m_IsReadable"].GetValue()); } - + [Test] public void AccessProperty_InvalidProperty_ThrowException() { var obj = GetObjectInfo(-7865028809519950684); - + var root = m_SerializedFile.GetTypeTreeRoot(obj.Id); var reader = new TypeTreeReaders.RandomAccessReader(m_SerializedFile, root, m_Reader, obj.Offset); - + Assert.Throws(() => reader["ThisIsAnUnexistingPropertyName"].GetValue()); } - + [Test] public void AccessReferencedObject_ValidProperty_ReturnExpectedValues() { var obj = GetObjectInfo(-4606375687431940004); - + var root = m_SerializedFile.GetTypeTreeRoot(obj.Id); var reader = new TypeTreeReaders.RandomAccessReader(m_SerializedFile, root, m_Reader, obj.Offset); @@ -632,10 +632,10 @@ public void AccessReferencedObject_ValidProperty_ReturnExpectedValues() id0 = reader["m_Item"]["rid"].GetValue(); id1 = reader["m_Item2"]["rid"].GetValue(); } - + Assert.IsTrue(reader["references"].HasChild($"rid({id0})")); Assert.IsTrue(reader["references"].HasChild($"rid({id1})")); - + Assert.AreEqual(1, reader["references"][$"rid({id0})"]["data"]["m_Data"].GetValue()); Assert.AreEqual("Ripe", reader["references"][$"rid({id0})"]["data"]["m_Description"].GetValue()); Assert.AreEqual(1, reader["references"][$"rid({id1})"]["data"]["m_Data"].GetValue()); diff --git a/UnityFileSystem/DllWrapper.cs b/UnityFileSystem/DllWrapper.cs index 14b7888..0917bf2 100644 --- a/UnityFileSystem/DllWrapper.cs +++ b/UnityFileSystem/DllWrapper.cs @@ -1,10 +1,10 @@ -using System; +using System; using System.Runtime.InteropServices; using System.Text; namespace UnityDataTools.FileSystem; -internal class UnityArchiveHandle : SafeHandle +public class UnityArchiveHandle : SafeHandle { public UnityArchiveHandle() : base(IntPtr.Zero, true) { @@ -18,7 +18,7 @@ protected override bool ReleaseHandle() } } -internal class UnityFileHandle : SafeHandle +public class UnityFileHandle : SafeHandle { public UnityFileHandle() : base(IntPtr.Zero, true) { @@ -32,7 +32,7 @@ protected override bool ReleaseHandle() } } -internal class SerializedFileHandle : SafeHandle +public class SerializedFileHandle : SafeHandle { public SerializedFileHandle() : base(IntPtr.Zero, true) { @@ -46,7 +46,7 @@ protected override bool ReleaseHandle() } } -internal class TypeTreeHandle : SafeHandle +public class TypeTreeHandle : SafeHandle { public TypeTreeHandle() : base(IntPtr.Zero, true) { @@ -62,7 +62,7 @@ protected override bool ReleaseHandle() internal IntPtr Handle => handle; } -internal enum ReturnCode +public enum ReturnCode { Success, AlreadyInitialized, @@ -83,10 +83,10 @@ internal enum ReturnCode [Flags] public enum ArchiveNodeFlags { - None = 0, - Directory = 1 << 0, - Deleted = 1 << 1, - SerializedFile = 1 << 2, + None = 0, + Directory = 1 << 0, + Deleted = 1 << 1, + SerializedFile = 1 << 2, } public enum CompressionType @@ -123,22 +123,22 @@ public struct ObjectInfo [Flags] public enum TypeTreeFlags { - None = 0, - IsArray = 1 << 0, - IsManagedReference = 1 << 1, - IsManagedReferenceRegistry = 1 << 2, - IsArrayOfRefs = 1 << 3, + None = 0, + IsArray = 1 << 0, + IsManagedReference = 1 << 1, + IsManagedReferenceRegistry = 1 << 2, + IsArrayOfRefs = 1 << 3, } [Flags] public enum TypeTreeMetaFlags { - None = 0, - AlignBytes = 1 << 14, - AnyChildUsesAlignBytes = 1 << 15, + None = 0, + AlignBytes = 1 << 14, + AnyChildUsesAlignBytes = 1 << 15, } -internal static class DllWrapper +public static class DllWrapper { [DllImport("UnityFileSystemApi", CallingConvention = CallingConvention.Cdecl, @@ -250,4 +250,4 @@ public static extern ReturnCode GetTypeTreeNodeInfo(TypeTreeHandle handle, int n StringBuilder name, int nameLen, out int offset, out int size, [MarshalAs(UnmanagedType.U4)] out TypeTreeFlags flags, [MarshalAs(UnmanagedType.U4)] out TypeTreeMetaFlags metaFlags, out int firstChildNode, out int nextNode); -} \ No newline at end of file +} diff --git a/UnityFileSystem/SerializedFile.cs b/UnityFileSystem/SerializedFile.cs index 829ea37..6791b6f 100644 --- a/UnityFileSystem/SerializedFile.cs +++ b/UnityFileSystem/SerializedFile.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; @@ -103,4 +103,4 @@ public void Dispose() m_Handle.Dispose(); } } -} \ No newline at end of file +} 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(); + } +} + diff --git a/UnityFileSystem/TypeTreeNode.cs b/UnityFileSystem/TypeTreeNode.cs index 220ab68..9a3ab49 100644 --- a/UnityFileSystem/TypeTreeNode.cs +++ b/UnityFileSystem/TypeTreeNode.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; @@ -29,7 +29,7 @@ public class TypeTreeNode // Child nodes container. public List Children => m_Children.Value; - + // True if the field has no child. public bool IsLeaf => m_FirstChildNodeIndex == 0; @@ -38,7 +38,7 @@ public class TypeTreeNode // True if the field is an array. public bool IsArray => ((int)Flags & (int)TypeTreeFlags.IsArray) != 0; - + // True if the field is a ManagedReferenceRegistry public bool IsManagedReferenceRegistry => ((int)Flags & (int)TypeTreeFlags.IsManagedReferenceRegistry) != 0; @@ -192,4 +192,4 @@ Type GetCSharpType() throw new Exception($"Unknown type {Type}"); } -} \ No newline at end of file +} diff --git a/UnityFileSystem/TypeTreeReaders/RandomAccessReader.cs b/UnityFileSystem/TypeTreeReaders/RandomAccessReader.cs index 7c450d9..4537a84 100644 --- a/UnityFileSystem/TypeTreeReaders/RandomAccessReader.cs +++ b/UnityFileSystem/TypeTreeReaders/RandomAccessReader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -79,7 +79,7 @@ public RandomAccessReader(SerializedFile serializedFile, TypeTreeNode node, Unit { // Create the referenced object reader. var refObjReader = new RandomAccessReader(m_SerializedFile, refObjNode, reader, curOffset, true); - + // A referenced object with null data means that we reached the end of the referenced objects. if (refObjReader["data"] == null) { @@ -94,7 +94,7 @@ public RandomAccessReader(SerializedFile serializedFile, TypeTreeNode node, Unit else if (version == 2) { // In version 2, referenced objects are stored in a vector. - + // Second child is the RefIds vector. var refIdsVectorNode = node.Children[1]; // RefIds vector's child is the Array. @@ -117,7 +117,7 @@ public RandomAccessReader(SerializedFile serializedFile, TypeTreeNode node, Unit } else { - throw new Exception("Unsupported ManagedReferenceRegistry version"); + throw new Exception($"Unsupported ManagedReferenceRegistry version {version}"); } } else if (isReferencedObject) @@ -236,7 +236,7 @@ RandomAccessReader GetChild(string name) if (m_ChildrenCacheObject.TryGetValue(name, out var nodeReader)) return nodeReader; - + if (m_TypeTreeNode.IsManagedReferenceRegistry) { // ManagedReferenceRegistry are handled differently. The children @@ -487,4 +487,4 @@ IEnumerator IEnumerable.GetEnumerator() { return new Enumerator(this); } -} \ No newline at end of file +} diff --git a/UnityFileSystem/UnityArchive.cs b/UnityFileSystem/UnityArchive.cs index d518bd2..3ceb20e 100644 --- a/UnityFileSystem/UnityArchive.cs +++ b/UnityFileSystem/UnityArchive.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; @@ -55,4 +55,4 @@ public void Dispose() m_Nodes = new Lazy>(() => GetArchiveNodes()); } } -} \ No newline at end of file +} diff --git a/UnityFileSystem/UnityFile.cs b/UnityFileSystem/UnityFile.cs index ebd5ca3..12e317a 100644 --- a/UnityFileSystem/UnityFile.cs +++ b/UnityFileSystem/UnityFile.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace UnityDataTools.FileSystem; @@ -43,4 +43,4 @@ public void Dispose() m_Handle.Dispose(); } } -} \ No newline at end of file +} diff --git a/UnityFileSystem/UnityFileReader.cs b/UnityFileSystem/UnityFileReader.cs index e287fd2..bf46145 100644 --- a/UnityFileSystem/UnityFileReader.cs +++ b/UnityFileSystem/UnityFileReader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Text; using Force.Crc32; @@ -8,10 +8,10 @@ namespace UnityDataTools.FileSystem; // This class can be used to read typed data from a UnityFile. Is uses a buffer for better performance. public class UnityFileReader : IDisposable { - UnityFile m_File; - byte[] m_Buffer; - long m_BufferStartInFile; - long m_BufferEndInFile; + UnityFile m_File; + byte[] m_Buffer; + long m_BufferStartInFile; + long m_BufferEndInFile; public long Length { get; } @@ -50,7 +50,7 @@ public void ReadArray(long fileOffset, int size, Array dest) var offset = GetBufferOffset(fileOffset, size); Buffer.BlockCopy(m_Buffer, offset, dest, 0, size); } - + public string ReadString(long fileOffset, int size) { var offset = GetBufferOffset(fileOffset, size); @@ -121,7 +121,7 @@ public uint ComputeCRC(long fileOffset, int size, uint crc32 = 0) { var readSize = size > m_Buffer.Length ? m_Buffer.Length : size; var readBytes = 0; - + while (readBytes < size) { var offset = GetBufferOffset(fileOffset, readSize); @@ -136,4 +136,4 @@ public void Dispose() { m_File.Dispose(); } -} \ No newline at end of file +} diff --git a/UnityFileSystem/UnityFileSystem.cs b/UnityFileSystem/UnityFileSystem.cs index a0abe7f..92612bf 100644 --- a/UnityFileSystem/UnityFileSystem.cs +++ b/UnityFileSystem/UnityFileSystem.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; namespace UnityDataTools.FileSystem; @@ -16,7 +16,7 @@ public static void Init() HandleErrors(r); } } - + public static void Cleanup() { // Uninitialize the native library. @@ -85,9 +85,9 @@ internal static void HandleErrors(ReturnCode returnCode, string filename = "") case ReturnCode.FileError: throw new IOException("File operation error."); - + case ReturnCode.TypeNotFound: throw new ArgumentException("Type not found."); } } -} \ No newline at end of file +} diff --git a/UnityFileSystem/UnityFileSystem.csproj b/UnityFileSystem/UnityFileSystem.csproj index cef3abc..7267f5a 100644 --- a/UnityFileSystem/UnityFileSystem.csproj +++ b/UnityFileSystem/UnityFileSystem.csproj @@ -1,7 +1,17 @@  - net6.0 + net9.0 + e934fe3f-4c03-4bed-8f71-e6213d731c9e + latest + + + + AnyCPU + + + + AnyCPU diff --git a/UnityFileSystem/UnityFileSystemApi.dll b/UnityFileSystem/UnityFileSystemApi.dll index fa1460d..4725fd3 100644 Binary files a/UnityFileSystem/UnityFileSystemApi.dll and b/UnityFileSystem/UnityFileSystemApi.dll differ diff --git a/UnityFileSystem/UnityFileSystemApi.dylib b/UnityFileSystem/UnityFileSystemApi.dylib index d4ed846..15f4ea7 100755 Binary files a/UnityFileSystem/UnityFileSystemApi.dylib and b/UnityFileSystem/UnityFileSystemApi.dylib differ diff --git a/UnityFileSystem/UnityFileSystemApi.so b/UnityFileSystem/UnityFileSystemApi.so index dbf19a1..7a0e540 100644 Binary files a/UnityFileSystem/UnityFileSystemApi.so and b/UnityFileSystem/UnityFileSystemApi.so differ diff --git a/UnityFileSystemTestData/Assets/Editor/BuildAssetBundles.cs b/UnityFileSystemTestData/Assets/Editor/BuildAssetBundles.cs index 6b6b643..afce564 100644 --- a/UnityFileSystemTestData/Assets/Editor/BuildAssetBundles.cs +++ b/UnityFileSystemTestData/Assets/Editor/BuildAssetBundles.cs @@ -4,22 +4,22 @@ public class BuildAssetBundles { - [MenuItem ("Tools/Generate AssetBundles")] - static void GenerateAssetBundles () + [MenuItem("Tools/Generate AssetBundles")] + static void GenerateAssetBundles() { Directory.CreateDirectory("AssetBundles"); - BuildPipeline.BuildAssetBundles ("AssetBundles", BuildAssetBundleOptions.None, BuildTarget.StandaloneOSX); - + BuildPipeline.BuildAssetBundles("AssetBundles", BuildAssetBundleOptions.None, BuildTarget.StandaloneOSX); + var outPath = Path.Combine(Directory.GetParent(Application.dataPath).Parent.FullName, "TestCommon", "Data", "AssetBundles", Application.unityVersion); - + Directory.CreateDirectory(outPath); File.Copy(Path.Combine("AssetBundles", "assetbundle"), Path.Combine(outPath, "assetbundle"), true); File.Copy(Path.Combine("AssetBundles", "scenes"), Path.Combine(outPath, "scenes"), true); } - - [MenuItem ("Tools/Generate PlayerData")] - static void GeneratePlayerData () + + [MenuItem("Tools/Generate PlayerData")] + static void GeneratePlayerData() { if (!EditorUtility.DisplayDialog("Warning!", "Make sure that the \"ForceAlwaysWriteTypeTrees\" Diagnostic Switch is enabled in the Editor Preferences (Diagnostic/Editor section)", @@ -27,12 +27,12 @@ static void GeneratePlayerData () { return; } - - string[] levels = new string[] {"Assets/Scenes/OtherScene.unity"}; + + string[] levels = new string[] { "Assets/Scenes/OtherScene.unity" }; BuildPipeline.BuildPlayer(levels, Path.Combine(Path.GetDirectoryName(Application.dataPath), "build", "game"), BuildTarget.StandaloneOSX, BuildOptions.None); - + var outPath = Path.Combine(Directory.GetParent(Application.dataPath).Parent.FullName, "TestCommon", "Data", "PlayerData", Application.unityVersion); - + Directory.CreateDirectory(outPath); File.Copy(Path.Combine("build", "game.app", "Contents", "Resources", "Data", "level0"), outPath, true); } diff --git a/global.json b/global.json new file mode 100644 index 0000000..f4fd385 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "9.0.0", + "rollForward": "latestMajor", + "allowPrerelease": true + } +} \ No newline at end of file