diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c57817c..54ae8b9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ All notable changes to this package will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [1.18.11] - 2021-06-15 +- Improved performance of Labels popup in Groups Window. +- Added "Copy Address to Clipboard" Context menu option in Groups Window. +- Added AssetLoadMode option to AddressableAssetsGroup, adds "Requested Asset And Dependencies" and "All Packed - Assets And Dependencies" load methods. +- (2021.2+) Improved performance of copying local buld path Groups built content when building a Player. +- Removed "Export Addressables" button from groups window because it was no longer in use. +- Fixed issue where loading remote catalog from .json fails when Compress Local Catalog is enabled. +- Fixed issue where loading remote catalog from bundle on WebGL fails when Compress Local Catalog is enabled. +- Added multi-project workflow documentation +- Made CacheInitializationData.ExpirationDelay obsolete +- Improve Hierarchical Search performance in Groups Window. +- Build now fails earlier if invalid or unsupported files are included. +- Fixed issue where renaming Group and Profiles would not cancel using Escape key. +- Fixed issue where StripUnityVersionFromBundleBuild and DisableVisibleSubAssetRepresentations were not being serialised to file. +- Updated content update docs to be a little more clear +- Made ExpirationDelay on the CacheInitializationObjects obsolete +- Reduced amount of main thread file I/O performed during AssetBundle loading + ## [1.18.9] - 2021-06-04 - Added "Select" button for Addressable Asset in Inspector to select the Asset in the Addressables Groups Window. - Reduced the number of file copies required during building Addressables and moving Addressables content during Player build. diff --git a/Documentation~/AddressableAssetSettings.md b/Documentation~/AddressableAssetSettings.md index 6cbaffaf..bd4b25e8 100644 --- a/Documentation~/AddressableAssetSettings.md +++ b/Documentation~/AddressableAssetSettings.md @@ -8,7 +8,7 @@ To access these settings, open the AddressableAssetSettings Inspector (menu: **W When you perform a content build, the build process saves the settings needed at runtime to the `settings.json` data file. When you run your application in the Editor using the `Use Existing Build (requires built groups)` Playmode Script, you must rebuild your Addressable content to update `settings.json` if you have made changes. If you enter Play mode using the `Use Asset Database (fastest)` or `Simulate Groups (advanced)` scripts, the Addressables system uses the current values in AddressableAssetSettings, so you do not need to rebuild. -![Addressable Asset Settings Inspector](images/AddressableAssetSettings.png)
+![Addressable Asset Settings Inspector](images/AddressableAssetSettingsWithPathPair.png)
*Addressable Asset Settings Inspector* #### Manage Groups @@ -41,8 +41,7 @@ The **Manage Profiles** button opens the Profiles window. You can also open the | **Disable Catalog Update on Startup** | Whether the Addressables system should skip the check for an updated content catalog when the Addressables system [initializes](InitializeAsync.md).
Note that you can update the catalog later using [Addressables.UpdateCatalogs](UpdateCatalogs.md). | | **Content State Build Path** | The path to the folder in which to save the addressables_content_state.bin file. If empty, the file is saved to Assets/AddressableAssetsData. | | **Build Remote Catalog** | Whether a remote catalog should be built-for and loaded-by the app. When enabled, content builds generate .json and .hash files for the catalog to **Build Path** and the Addressables system loads these files from **Load Path** at runtime. The system caches the catalog and compares the remote .hash file to the cached version to determine if the catalog itself should be updated (along with any changed AssetBundles). In order to update content in an existing, built app, you must build and host a remote catalog. Overwriting the catalog is how the app gets informed of the updated content. See [Profiles](AddressableAssetsProfiles.md) for more information of configuring build and load paths.| -| **- Build Path** | The path at which to build the content catalog for online retrieval. Typically, this path should be the same as the build path that you use for your remote Addressables groups, such as the RemoteBuildPath profile variable.| -| **- Load Path** | The path or URL from which to load the remote content catalog. Typically, this path should be the same as the load path that you use for your remote Addressables groups, such as the RemoteLoadPath profile variable. It is your responsibility to copy or upload the remote catalog files so that your app can access them at the specified location. | +| **- Build & Load Paths** | The pair of paths that determine where to build the content catalog for online retreival and the path or URL from which to load the remote content catalog.
For the build path, typically, this path should be the same as the build path that you use for your remote Addresssables groups, such as the `RemoteBuildPath` profile variable.
For the load path, typically this path should be the same as the load path that you use for your remote Addressables groups, such as the `RemoteLoadPath` profile variable. It is your responsibility to copy or upload the remote catalog files so that your app can access them at the specifiedc location. #### Downloads | **Property:** | **Function:** | @@ -57,7 +56,7 @@ The **Manage Profiles** button opens the Profiles window. You can also open the | **Ignore Invalid/Unsupported Files in Build** | Whether unsupported files during build should be ignored or treated as an error. | | **Unique Bundle IDs** | When enabled, AssetBundles are assigned unique, more complex internal identifiers. This may result in more bundles being rebuilt. See [Content Update Workflow](ContentUpdateWorkflow.md#unique-bundle-ids) for more information. | | **Contiguous Bundles** | When enabled, the Addressables build script packs assets in bundles contiguously based on the ordering of the source asset, which results in improved asset loading times. Unity recommends that you enable this option. However, enabling this option does result in binary differences in the bundles produced. Disable this option if you've built bundles with a version of Addressables older than 1.12.1 and you want to minimize bundle changes. | -| **Non-recursive Dependency Calculation** | Calculate and build asset bundles using Non-Recursive Dependency calculation methods. This approach helps reduce asset bundle rebuilds and runtime memory consumption. Unity recommends that you enable this option. However, enabling this option does result in binary differences in the bundles produced.
**Requires Unity 2020.2.1 or above** | +| **Non-recursive Dependency Calculation** | Calculate and build asset bundles using Non-Recursive Dependency calculation methods. This approach helps reduce asset bundle rebuilds and runtime memory consumption. Unity recommends that you enable this option. However, enabling this option does result in binary differences in the bundles produced.
**Requires Unity 2019.4.19f1 or above** | | **Shader Bundle Naming Prefix** | Sets the naming convention used for the Unity built in shader bundle at build time.
The recommended setting is Project Name. | | **- Shader Bundle Custom Prefix** | Custom Unity built-in shader bundle prefix that is used if AddressableAssetSettings.ShaderBundleNaming is set to ShaderBundleNaming.Custom. | | **Mono Bundle Naming Prefix** | Sets the naming convention used for the MonoScript bundle at build time. A MonoScript contains information for loading the corresponding runtime class.
The recommended setting is Project Name | diff --git a/Documentation~/AddressableAssetsProfiles.md b/Documentation~/AddressableAssetsProfiles.md index f1035d8a..a22cc91b 100644 --- a/Documentation~/AddressableAssetsProfiles.md +++ b/Documentation~/AddressableAssetsProfiles.md @@ -65,7 +65,11 @@ Once you set up the necessary variables in your profile, you can select the buil To set your build and load paths: 1. Select an Addressable Assets group from the **Project** window. -2. In its related **Inspector** window, under **Content Packing & Loading** > **Build and Load Paths**, select the desired variables from the currently set profile in the drop-downs for **Build Path** and **Load Path**.
Notice that you do not enter the path directly, but rather select the variable representing the path defined in the **Profiles** window earlier. Once selected, the path displays under the drop-down but is not editable here.
Be careful to ensure the build and load paths are a matched pair. For example, if you are building to the local path, you cannot load from a server. +2. In its related **Inspector** window, under **Content Packing & Loading** > **Build and Load Paths**, select the desired variables from the currently set profile in the drop-downs for **Build Path** and **Load Path**.
Notice that you do not enter the path directly, but rather select the variable representing the path defined in the **Profiles** window earlier. Once selected, the path displays under the drop-down but is not editable here.
Be careful when utilizing the `` setting and ensure that the build and load paths are a matched pair. For example, if you are building to the local path, you cannot load from a server. +![Selecting a path pair of variables.](images/InspectorPathPair.png)
+_Selecting a path pair in the inspector window._ +![Selecting a path pair of variables.](images/InspectorCustomPathPair.png)
+_Selecting a custom path pair in the inspector window._ ## Examples Consider the following example, demonstrating the local development phase of your content. diff --git a/Documentation~/AddressablesFAQ.md b/Documentation~/AddressablesFAQ.md index e1dde86e..a6b7a219 100644 --- a/Documentation~/AddressablesFAQ.md +++ b/Documentation~/AddressablesFAQ.md @@ -37,7 +37,7 @@ Currently there are two optimizations available. 2. Disable built-in scenes and Resources. Addressables provides the ability to load content from Resources and from the built-in scenes list. By default this feature is on, which can bloat the catalog if you do not need this feature. To disable it, select the "Built In Data" group within the Groups window (**Window** > **Asset Management** > **Addressables** > **Groups**). From the settings for that group, you can uncheck "Include Resources Folders" and "Include Build Settings Scenes". Unchecking these options only removes the references to those asset types from the Addressables catalog. The content itself is still built into the player you create, and you can still load it via legacy API. ### What is addressables_content_state? -After every content build of Addressables, we produce an addressables_content_state.bin file, which is saved to the folder path defined in the Addressable Assets Settings value "Content State build Path" appended with /. If this value is empty, the default location will be the `Assets/AddressableAssetsData//` folder of your Unity project. +After every new Addressable content build, we produce an addressables_content_state.bin file, which is saved to the folder path defined in the Addressable Assets Settings value "Content State build Path" appended with /. A new content build here is defined as a content build that is not part of the [content update workflow](ContentUpdateWorkflow.md). If this value is empty, the default location will be the `Assets/AddressableAssetsData//` folder of your Unity project. This file is critical to our [content update workflow](ContentUpdateWorkflow.md). If you are not doing any content updates, you can completely ignore this file. If you are planning to do content updates, you will need the version of this file produced for the previous release. We recommend checking it into version control and creating a branch each time you release a player build. More information is available on our [content update workflow page](ContentUpdateWorkflow.md). @@ -48,6 +48,20 @@ As your project grows larger, keep an eye on the following aspects of your asset * Group hierarchy display - Another UI-only option to help with scale is **Group Hierarchy with Dashes**. The option is available in the groups window under **Tools** > **Groups View** > **Group Hierarchy with Dashes**. With this enabled, groups that contain dashes '-' in their names will display as if the dashes represented folder hierarchy. This does not affect the actual group name, or the way things are built. For example, two groups called "x-y-z" and "x-y-w" would display as if inside a folder called "x", there was a folder called "y". Inside that folder were two groups, called "x-y-z" and "x-y-w". This will not really affect UI responsiveness, but simply makes it easier to browse a large collection of groups. * Bundle layout at scale - For more information about how best to set up your layout, see the earlier question: [_Is it better to have many small bundles or a few bigger ones_](AddressablesFAQ.md#Is-it-better-t-have-many-small-bundles-or-a-few-bigger-ones) +### What Asset Load Mode to use? +For most platforms and collection of content, it is recommended to use `Requested Asset and Dependencies`. This mode will only load what is required for the Assets requested with `LoadAssetAsync` or `LoadAssetsAsync`. +This prevents situations where Assets are loaded into memory that are not used. + +Performance in situations where you will load all Assets that are packed together, such as a loading screen. Most types of content will have either have similar or improved performance when loading each individually using `Requested Asset and Dependencies` mode. +Loading performance can vary between content type. As an example, large counts of serialised data such as Prefabs or ScriptableObjects with direct references to other serialised data will load faster using `All Packed Assets and Dependencies`. With some other Assets like Textures, it is often more performant to load each Asset individually. +If using [Synchronous Addressables](SynchronousAddressables.md), there is little performance between between Asset load modes. Because of greater flexibility it is recommended to use `Requested Asset and Dependencies` where you know the content will be loaded synchronously. + +**Note**: The above examples are taken for Desktop and Mobile. Performance may differ between platforms. `All Packed Assets and Dependencies` mode typically performs better than loading assets individually on the Nintendo Switch. +It is recommended to profile loading performance for your specific content and platform to see what works for your Application. + +On loading the first Asset with `All Packed Assets and Dependencies`, all Assets are loaded into memory. Later LoadAssetAsync calls for Assets from that pack will return the preloaded Asset without needing to load it. +Even though all the Assets in a group and any dependencies are loaded in memory when you use the All Packed Assets and Dependencies option, the reference count of an individual asset is not incremented unless you explicitly load it (or it is a dependency of an asset that you load explicitly). If you later call [`Resources.UnloadUnusedAssets`](https://docs.unity3d.com/ScriptReference/Resources.UnloadUnusedAssets.html), or you load a new Scene using [`LoadSceneMode.Single`](https://docs.unity3d.com/ScriptReference/SceneManagement.LoadSceneMode.html), then any unused assets (those with a reference count of zero) are unloaded. + ### Is it safe to edit loaded Assets? When editing Assets loaded from Bundles, in a Player or when using "Use Existing Build (requires built groups)" playmode setting. The Assets are loaded from the Bundle and only exist in memory. Changes cannot be written back to the Bundle on disk, and any modifications to the Object in memory do not persist between sessions. diff --git a/Documentation~/ContentUpdateWorkflow.md b/Documentation~/ContentUpdateWorkflow.md index bfa64b6f..050f5697 100644 --- a/Documentation~/ContentUpdateWorkflow.md +++ b/Documentation~/ContentUpdateWorkflow.md @@ -9,6 +9,8 @@ Unity recommends structuring your game content into two categories: * `Cannot Change Post Release`: Static content that you never expect to update. * `Can Change Post Release`: Dynamic content that you expect to update. +A `Release` in this context technically refers to a release of your built Addressable content, which likely happens alongside a player build and release if a player build hasn't already been released. After a player build has been released, these game content categories are relevant to any content update releases. New remote Addressable Groups can be built into a content update without the need to release a new player build. + In this structure, content marked as `Cannot Change Post Release` ships with the application (or downloads soon after install), and resides in very few large bundles. Content marked as `Can Change Post Release` resides online, ideally in smaller bundles to minimize the amount of data needed for each update. One of the goals of the Addressable Assets System is to make this structure easy to work with and modify without having to change your scripts. However, the Addressable Assets System can also accommodate situations that require changes to the content marked as `Cannot Change Post Release`, when you don't want to publish a whole new application build. Modified assets and their dependencies (and dependents) will be duplicated in new bundles that will be used instead of the shipped content. This can result in a much smaller update than replacing the entire bundle or rebuilding the game. Once a build has been made, it is important to NOT change the state of a group from "Cannot Change Post Release" to "Can Change Post Release" or vice versa until an entirely new build is made. If the groups change after a full content build but before a content update, Addressables will not be able to generate the correct changes needed for the update. @@ -18,10 +20,12 @@ Note that in cases that do not allow remote updates (such as many of the current ## How it works Addressables uses a content catalog to map an address to each Asset, specifying where and how to load it. In order to provide your app with the ability to modify that mapping, your original app must be aware of an online copy of this catalog. To set that up, enable the **Build Remote Catalog** setting on the [`AddressableAssetSettings`](xref:UnityEditor.AddressableAssets.Settings.AddressableAssetSettings) Inspector. This ensures that a copy of the catalog gets built to and loaded from the specified paths. This load path cannot change once your app has shipped. The content update process creates a new version of the catalog (with the same file name) to overwrite the file at the previously specified load path. -Building an application generates a unique app content version string, which identifies what content catalog each app should load. A given server can contain catalogs of multiple versions of your app without conflict. We store the data we need in the `addressables_content_state.bin` file. This includes the version string, along with hash information for any Asset that is contained in a group marked as `Cannot Change Post Release`. By default, this is located in the `Assets/AddressableAssetsData/` Project directory, or path assigned to `AddressbleAssetSettings` value `ContentStateBuildPath` appended with `/`. Where `` is your target platform. +Building an application generates a unique app content version string, which identifies what content catalog each app should load. A given server can contain catalogs of multiple versions of your app without conflict. We store the data we need in the `addressables_content_state.bin` file. The bin file is generated for new content builds only. It includes the version string, along with hash information for any Asset that is contained in a group marked as `Cannot Change Post Release`. By default, this is located in the `Assets/AddressableAssetsData/` Project directory, or path assigned to `AddressbleAssetSettings` value `ContentStateBuildPath` appended with `/`. Where `` is your target platform. The `addressables_content_state.bin` file contains hash and dependency information for every `Cannot Change Post Release` Asset group in the Addressables system. All groups building to the `StreamingAssets` folder should be marked as `Cannot Change Post Release`, though large remote groups may also benefit from this designation. During the next step (preparing for content update, described below), this hash information determines if any `Cannot Change Post Release` groups contain changed Assets, and thus need those Assets moved elsewhere. +For more information about the `addressables_content_state.bin`, check out the [FAQ page](AddressablesFAQ.md). + ### Update life cycle The first step in the process of building content is always a fresh full build. This can be triggered from within the **Addressables Groups** window in the Unity Editor (**Window** > **Asset Management** > **Addressables** > **Groups**). Once there, selecting your build script from **Build** > **New Build**. Unless you create a custom build, the only option will be **Default Build Script**. diff --git a/Documentation~/MemoryManagement.md b/Documentation~/MemoryManagement.md index fd935c9f..90e78bda 100644 --- a/Documentation~/MemoryManagement.md +++ b/Documentation~/MemoryManagement.md @@ -16,6 +16,8 @@ To unload the Asset, use the [`Addressables.Release`](xref:UnityEngine.Addressab **Note**: The Asset may or may not be unloaded immediately, contingent on existing dependencies. For more information, read the section on [when memory is cleared](#when-is-memory-cleared). +**Note**: If Asset Load Mode `All Packed Assets and Dependencies` is used in "Content Packing & Loading" settings, the first Asset loaded from the pack will load all Assets at the same time. This can cause a delay in expected time to load the first Asset, subsequent Asset loads will be preloaded and complete quickly. This can lead to Assets in memory with no ref count until they have been requested with Addressables Loading API and could be unloaded from memory, and will be reloaded individually on request. For more information, read the section on [when memory is cleared](#when-is-memory-cleared). + ### Scene loading To load a Scene, use [`Addressables.LoadSceneAsync`](xref:UnityEngine.AddressableAssets.AssetReference.LoadSceneAsync(UnityEngine.SceneManagement.LoadSceneMode,System.Boolean,System.Int32)). You can use this method to load a Scene in `Single` mode, which closes all open Scenes, or in `Additive` mode (for more information, see documentation on [Scene mode loading](https://docs.unity3d.com/ScriptReference/SceneManagement.LoadSceneMode.html)). diff --git a/Documentation~/ModificationEvents.md b/Documentation~/ModificationEvents.md new file mode 100644 index 00000000..df273aea --- /dev/null +++ b/Documentation~/ModificationEvents.md @@ -0,0 +1,94 @@ +# Modification Events +Modification events are used to signal to parts of the Addressables system when certain data is manipulated, such as an `AddressableAssetGroup` or an `AddressableAssetEntry` getting added or removed. + +Modification events are triggered as part of `SetDirty` calls inside of Addressables. `SetDirty` is used to indicate when an asset needs to be re-serialized by the `AssetDatabase`. As part of this `SetDirty`, two modification event callbacks can trigger: +- `public static event Action OnModificationGlobal` +- `public Action OnModification { get; set; }` + +which can be found on [`AddressableAssetSettings`](xref:UnityEditor.AddressableAssets.Settings.AddressableAssetSettings) through a static, or instance, accessors respectively. + +#### Code Samples +``` +AddressableAssetSettings.OnModificationGlobal += (settings, modificationEvent, data) => + { + if(modificationEvent == AddressableAssetSettings.ModificationEvent.EntryAdded) + { + //Do work + } + }; + + AddressableAssetSettingsDefaultObject.Settings.OnModification += (settings, modificationEvent, data) => + { + if (modificationEvent == AddressableAssetSettings.ModificationEvent.EntryAdded) + { + //Do work + } + }; +``` +Modification events pass in a generic `object` for the data associated with the event. Below is a list of the modification events and the data types that are passed with them. + +#### The Data Passed with Each ModificationEvent: +- GroupAdded +The data passed with this event is the [`AddressableAssetGroup`](xref:UnityEditor.AddressableAssets.Settings.AddressableAssetGroup), or list of groups, that were added. +- GroupRemoved +The data passed with this event is the [`AddressableAssetGroup`](xref:UnityEditor.AddressableAssets.Settings.AddressableAssetGroup), or list of groups, that were removed. +- GroupRenamed +The data passed with this event is the [`AddressableAssetGroup`](xref:UnityEditor.AddressableAssets.Settings.AddressableAssetGroup), or list of groups, that were renamed. +- GroupSchemaAdded +The data passed with this event is the [`AddressableAssetGroup`](xref:UnityEditor.AddressableAssets.Settings.AddressableAssetGroup), or list of groups, that had schemas added to them. +- GroupSchemaRemoved +The data passed with this event is the [`AddressableAssetGroup`](xref:UnityEditor.AddressableAssets.Settings.AddressableAssetGroup), or list of groups, that had schemas removed from them. +- GroupSchemaModified +The data passed with this event is the [`AddressableAssetGroupSchema`](xref:UnityEditor.AddressableAssets.Settings.AddressableAssetGroupSchema) that was modified. +- GroupTemplateAdded +The data passed with this event is the `ScriptableObject`, typically one that implements [`IGroupTemplate`](xref:UnityEditor.AddressableAssets.Settings.IGroupTemplate), that was the added Group Template object. +- GroupTemplateRemoved +The data passed with this event is the `ScriptableObject`, typically one that implements [`IGroupTemplate`](xref:UnityEditor.AddressableAssets.Settings.IGroupTemplate), that was the removed Group Template object. +- GroupTemplateSchemaAdded +The data passed with this event is the [`AddressableAssetGroupTemplete`](xref:UnityEditor.AddressableAssets.Settings.AddressableAssetGroupTemplete) that had a schema added. +- GroupTemplateSchemaRemoved +The data passed with this event is the [`AddressableAssetGroupTemplete`](xref:UnityEditor.AddressableAssets.Settings.AddressableAssetGroupTemplete) that had a schema removed. +- EntryCreated +The data passed with this event is the [`AddressableAssetEntry`](xref:UnityEditor.AddressableAssets.Settings.AddressableAssetEntry) that was created. +- EntryAdded +The data passed with this event is the [`AddressableAssetEntry`](xref:UnityEditor.AddressableAssets.Settings.AddressableAssetEntry), or list of entries, that were added. +- EntryMoved +The data passed with this event is the [`AddressableAssetEntry`](xref:UnityEditor.AddressableAssets.Settings.AddressableAssetEntry), or list of entries, that were moved from one group to another. +- EntryRemoved +The data passed with this event is the [`AddressableAssetEntry`](xref:UnityEditor.AddressableAssets.Settings.AddressableAssetEntry), or list of entries, that were removed. +- LabelAdded +The data passed with this event is the `string` label that was added. +- LabelRemoved +The data passed with this event is the `string` label that was removed. +- ProfileAdded +The data passed with this event is [`BuildProfile`](xref:UnityEditor.AddressableAssets.Settings.BuildProfile) that was added. +- ProfileRemoved +The data passed with this event is the `string` of the profile ID that was removed. +- ProfileModified +The data passed with this event is [`BuildProfile`](xref:UnityEditor.AddressableAssets.Settings.BuildProfile) that was modified, or `null` if a batch of `BuildProfiles` were modified. +- ActiveProfileSet +The data passed with this event if the `string` of the profile ID that is set as the active profile. +- EntryModified +The data passed with this event is the [`AddressableAssetEntry`](xref:UnityEditor.AddressableAssets.Settings.AddressableAssetEntry), or list of entries, that were modified. +- BuildSettingsChanged +The data passed with this event is the [`AddressableAssetBuildSettings`](xref:UnityEditor.AddressableAssets.Settings.AddressableAssetBuildSettings) object that was modified. +- ActiveBuildScriptChanged +The data passed with this event is the [`IDataBuilder`](xref:UnityEditor.AddressableAssets.Build.IDataBuilder) build script that was set as the active builder. +- DataBuilderAdded +The data passed with this event is the `ScriptableObject`, typically one that implements [`IDataBuilder`](xref:UnityEditor.AddressableAssets.Build.IDataBuilder), that was added to the list of DataBuilders. +- DataBuilderRemoved +The data passed with this event is the `ScriptableObject`, typically one that implements [`IDataBuilder`](xref:UnityEditor.AddressableAssets.Build.IDataBuilder), that was removed from the list of DataBuilders. +- InitializationObjectAdded +The data passed with this event is the `ScriptableObject`, typically one that implements [`IObjectInitializationDataProvider`](xref:UnityEngine.ResourceManagement.Util.IObjectInitializationDataProvider), that was added to the list of InitializationObjects. +- InitializationObjectRemoved +The data passed with this event is the `ScriptableObject`, typically one that implements [`IObjectInitializationDataProvider`](xref:UnityEngine.ResourceManagement.Util.IObjectInitializationDataProvider), that was removed from the list of InitializationObjects. +- ActivePlayModeScriptChanged +The data passed with this event is the [`IDataBuilder`](xref:UnityEditor.AddressableAssets.Build.IDataBuilder) that was set as the new active play mode data builder. +- BatchModification +The data passed with this event is `null`. This event is primarily used to indicate several modification events happening at the same time and the [`AddressableAssetSettings`](xref:UnityEditor.AddressableAssets.Settings.AddressableAssetSettings) object needed to be marked dirty. +- HostingServicesManagerModified +The data passed is either going to be the [`HostingServicesManager`](xref:UnityEditor.AddressableAssets.HostingServices.HostingServicesManager), or the [`HttpHostingService`](xref:UnityEditor.AddressableAssets.HostingServices.HttpHostingService) that were modified. +- GroupMoved +The data passed with this event is the full list of [`AddressableAssetGroups`](xref:UnityEditor.AddressableAssets.Settings.AddressableAssetGroup). +- CertificateHandlerChanged +The data passed with this event is the new `System.Type` of the Certificate Handler to be used. \ No newline at end of file diff --git a/Documentation~/MultiProject.md b/Documentation~/MultiProject.md new file mode 100644 index 00000000..4fe342b5 --- /dev/null +++ b/Documentation~/MultiProject.md @@ -0,0 +1,27 @@ +# Loading from Multiple Projects + +Should your situation require a multi-project workflow, such as a large project broken up across multiple Unity projects that have the Addressables package installed, we have [`Addressables.LoadContentCatalogAsync`](LoadContentCatalogAsync.md) to link together code and content across the various projects. Studios with teams that works on many facets of an application simultaneously may find benefit with this workflow. + +## Setting up multiple projects +The main items to note for a multi-project setup is to make sure: +1. Each project uses the same version of the Unity Editor +2. Each project uses the same version of the Addressables package + +From there projects can contain whatever you see fit for your given situation. One of your projects must be your "main project" or "source project". This is the project that you'll actually build and deploy your game binaries from. Typically, this source project is primarily comprised of code and very little to no content. The main piece of content that you would want in the primary project would be a bootstrap scene at minimum. It may also be desirable to include any scenes that need to be local for performance purposes before any AssetBundles have had a chance to be downloaded and cached. + +Secondary projects are, in most cases, the exact opposite. Mostly content and little to no code. **These projects need to have all remote Addressable Groups and Build Remote Catalog turned on.** Any local data built into these projects cannot be loaded in your source project's application. Non-critical scenes can live in these projects and be downloaded by the primary project when requested. + +#### The Typical Workflow +Once you have your projects setup, the workflow generally is as follows: +1. Build remote content for all secondary projects +2. Build Addressables content for source project +3. Start source project Play Mode or build source project binaries +4. In source project, use [`Addressables.LoadContentCatalogAsync`](LoadContentCatalogAsync.md) to load the remote catalogs of your other various projects +5. Proceed with game runtime as normal. Now that the catalogs are loaded Addressables is able to load assets from any of these locations. + +In regards to the source project, it should be noted that it may be worth having some minimal amount of content built locally with that project. Each project is unique, and so has unique needs, but having a small set of content needed to run your game in the event of internet connection issues or other various problems may be advisable. + +#### Handling Shaders +Addressables builds a Unity built in shader bundle for each set of Addressables player data that gets built. This means that when multiple AssetBundles are loaded that were built in secondary projects, there could be multiple built in shader bundles loaded at the same time. + +Depending on your specific situation, you may need to utilize the Shader Bundle Naming Prefix on the `AddressableAssetSettings` object. Each built in shader bundle needs to be named different from others built in your other projects. If they're not named differently you'll get `The AssetBundle [bundle] can't be loaded because another AssetBundle with the same files is already loaded.` errors. \ No newline at end of file diff --git a/Documentation~/TableOfContents.md b/Documentation~/TableOfContents.md index dcbd0ebe..6d5b7e4d 100644 --- a/Documentation~/TableOfContents.md +++ b/Documentation~/TableOfContents.md @@ -61,6 +61,7 @@ * [Address Lookup](AddressablesFAQ.md#is-it-possible-to-retrieve-the-address-of-an-asset-or-reference-at-runtime) * [Building and Recompiling Scripts](AddressablesFAQ.md#can-i-build-addressables-when-recompiling-scripts) * [Using Addressables and CI Pipelines](ContinuousIntegration.md) + * [Loading Content from Multiple Projects](MultiProject.md) * [Expanded API documentation](AddressablesAPI.md) * [AddressableAssetSettings](AddressableAssetSettings.md) * [BuildPlayerContent](BuildPlayerContent.md) @@ -75,3 +76,4 @@ * [LoadSceneAsync](LoadSceneAsync.md) * [TransformInternalId](TransformInternalId.md) * [UpdateCatalogs](UpdateCatalogs.md) + * [Modification Events](ModificationEvents.md) diff --git a/Documentation~/images/AddressableAssetSettingsWithPathPair.png b/Documentation~/images/AddressableAssetSettingsWithPathPair.png new file mode 100644 index 00000000..c3cbbc9a Binary files /dev/null and b/Documentation~/images/AddressableAssetSettingsWithPathPair.png differ diff --git a/Documentation~/images/InspectorCustomPathPair.png b/Documentation~/images/InspectorCustomPathPair.png new file mode 100644 index 00000000..4bd364d2 Binary files /dev/null and b/Documentation~/images/InspectorCustomPathPair.png differ diff --git a/Documentation~/images/InspectorPathPair.png b/Documentation~/images/InspectorPathPair.png new file mode 100644 index 00000000..65560f25 Binary files /dev/null and b/Documentation~/images/InspectorPathPair.png differ diff --git a/Editor/Build/BuildPipelineTasks/GenerateLocationListsTask.cs b/Editor/Build/BuildPipelineTasks/GenerateLocationListsTask.cs index e2983677..404e35bc 100644 --- a/Editor/Build/BuildPipelineTasks/GenerateLocationListsTask.cs +++ b/Editor/Build/BuildPipelineTasks/GenerateLocationListsTask.cs @@ -11,6 +11,7 @@ using UnityEngine; using UnityEngine.AddressableAssets.ResourceLocators; using UnityEngine.ResourceManagement.ResourceProviders; +using UnityEngine.ResourceManagement.Util; using static UnityEditor.AddressableAssets.Settings.AddressablesFileEnumeration; namespace UnityEditor.AddressableAssets.Build.BuildPipelineTasks @@ -165,7 +166,7 @@ internal static Output RunInternal(Input input) // Create a bundle entry for every bundle that our assets could reference foreach (List files in input.AssetToFiles.Values) files.ForEach(x => GetOrCreateBundleEntry(input.FileToBundle[x], bundleToEntry)); - + // build list of assets each bundle has as well as the dependent bundles using (input.Logger.ScopedStep(LogLevel.Info, "Calculate Bundle Dependencies")) { @@ -208,19 +209,8 @@ internal static Output RunInternal(Input input) var schema = bEntry.Group.GetSchema(); foreach (GUID assetGUID in bEntry.Assets) { - if (guidToEntry.TryGetValue(assetGUID.ToString(), out AddressableAssetEntry entry)) - { - if (entry.guid.Length > 0 && entry.address.Contains("[") && entry.address.Contains("]")) - throw new Exception($"Address '{entry.address}' cannot contain '[ ]'."); - if (entry.MainAssetType == typeof(DefaultAsset) && !AssetDatabase.IsValidFolder(entry.AssetPath)) - { - if (input.Settings.IgnoreUnsupportedFilesInBuild) - Debug.LogWarning($"Cannot recognize file type for entry located at '{entry.AssetPath}'. Asset location will be ignored."); - else - throw new Exception($"Cannot recognize file type for entry located at '{entry.AssetPath}'. Asset import failed for using an unsupported file type."); - } + if (guidToEntry.TryGetValue(assetGUID.ToString(), out AddressableAssetEntry entry)) entry.CreateCatalogEntriesInternal(locations, true, assetProvider, bEntry.ExpandedDependencies.Select(x => x.BundleName), null, input.AssetToAssetInfo, providerTypes, schema.IncludeAddressInCatalog, schema.IncludeGUIDInCatalog, schema.IncludeLabelsInCatalog, bEntry.AssetInternalIds); - } } } } @@ -272,13 +262,25 @@ internal static string GetLoadPath(AddressableAssetGroup group, string name, Bui return string.Empty; } - var loadPath = bagSchema.LoadPath.GetValue(group.Settings) + PathSeparatorForPlatform(target) + name; + string loadPath = bagSchema.LoadPath.GetValue(group.Settings); + loadPath = loadPath.Replace('\\', '/'); + if (loadPath.EndsWith("/")) + loadPath += name; + else + loadPath = loadPath + "/" + name; + if (!string.IsNullOrEmpty(bagSchema.UrlSuffix)) loadPath += bagSchema.UrlSuffix; + if (!ResourceManagerConfig.ShouldPathUseWebRequest(loadPath) && !bagSchema.UseUnityWebRequestForLocalBundles) + { + char separator = PathSeparatorForPlatform(target); + if (separator != '/') + loadPath = loadPath.Replace('/', separator); + } return loadPath; } - static char PathSeparatorForPlatform(BuildTarget target) + internal static char PathSeparatorForPlatform(BuildTarget target) { switch (target) { diff --git a/Editor/Build/DataBuilders/BuildScriptBase.cs b/Editor/Build/DataBuilders/BuildScriptBase.cs index 7db59578..3072a5b0 100644 --- a/Editor/Build/DataBuilders/BuildScriptBase.cs +++ b/Editor/Build/DataBuilders/BuildScriptBase.cs @@ -82,11 +82,19 @@ public TResult BuildData(AddressablesDataBuilderInput builderInput) whe AddressablesRuntimeProperties.ClearCachedPropertyValues(); - TResult result; + TResult result = default; // Append the file registry to the results using (m_Log.ScopedStep(LogLevel.Info, $"Building {this.Name}")) { - result = BuildDataImplementation(builderInput); + try + { + result = BuildDataImplementation(builderInput); + } + catch (Exception e) + { + Debug.LogError(e.Message); + return AddressableAssetBuildResult.CreateResult(null, 0, e.Message); + } if (result != null) result.FileRegistry = builderInput.Registry; } diff --git a/Editor/Build/DataBuilders/BuildScriptPackedMode.cs b/Editor/Build/DataBuilders/BuildScriptPackedMode.cs index 2cc3ca31..c3ddf1b4 100644 --- a/Editor/Build/DataBuilders/BuildScriptPackedMode.cs +++ b/Editor/Build/DataBuilders/BuildScriptPackedMode.cs @@ -156,7 +156,7 @@ internal string GetBuiltInShaderBundleName(AddressableAssetsBuildContext aaConte return value; } - + internal string GetMonoScriptBundleName(AddressableAssetsBuildContext aaContext) { string value = null; @@ -491,6 +491,8 @@ internal ReturnCode CreateCatalogBundle(string filepath, string jsonText, Addres }; var buildParams = new BundleBuildParameters(builderInput.Target, builderInput.TargetGroup, Path.GetDirectoryName(filepath)); + if (builderInput.Target == BuildTarget.WebGL) + buildParams.BundleCompression = BuildCompression.LZ4Runtime; var retCode = ContentPipeline.BuildAssetBundles(buildParams, bundleBuildContent, out IBundleBuildResults result, buildTasks, m_Log); if (Directory.Exists(tempFolderPath)) @@ -534,7 +536,7 @@ internal static void SetAssetEntriesBundleFileIdToCatalogEntryBundleFileId(IColl out ContentCatalogDataEntry catalogEntry)) { loc.BundleFileId = catalogEntry.InternalId; - + //This is where we strip out the temporary hash added to the bundle name for Content Update for the AssetEntry if (loc.parentGroup?.GetSchema()?.BundleNaming == BundledAssetGroupSchema.BundleNamingStyle.NoHash) @@ -683,7 +685,7 @@ internal static List HandleDuplicateBundleNames(List b bundleBuild.assetBundleName = hashedAssetBundleName; bundleInputDefs[i] = bundleBuild; - if(bundleToAssetGroup != null) + if (bundleToAssetGroup != null) bundleToAssetGroup.Add(hashedAssetBundleName, assetGroupGuid); } return generatedUniqueNames; @@ -736,6 +738,8 @@ internal static List PrepGroupBundlePacking(AddressableAs var combinedEntries = new List(); var packingMode = schema.BundleMode; var namingMode = schema.InternalBundleIdMode; + bool ignoreUnsupportedFilesInBuild = assetGroup.Settings.IgnoreUnsupportedFilesInBuild; + switch (packingMode) { case BundledAssetGroupSchema.BundlePackingMode.PackTogether: @@ -748,7 +752,7 @@ internal static List PrepGroupBundlePacking(AddressableAs a.GatherAllAssets(allEntries, true, true, false, entryFilter); } combinedEntries.AddRange(allEntries); - GenerateBuildInputDefinitions(allEntries, bundleInputDefs, CalculateGroupHash(namingMode, assetGroup, allEntries), "all"); + GenerateBuildInputDefinitions(allEntries, bundleInputDefs, CalculateGroupHash(namingMode, assetGroup, allEntries), "all", ignoreUnsupportedFilesInBuild); } break; case BundledAssetGroupSchema.BundlePackingMode.PackSeparately: { @@ -759,7 +763,7 @@ internal static List PrepGroupBundlePacking(AddressableAs var allEntries = new List(); a.GatherAllAssets(allEntries, true, true, false, entryFilter); combinedEntries.AddRange(allEntries); - GenerateBuildInputDefinitions(allEntries, bundleInputDefs, CalculateGroupHash(namingMode, assetGroup, allEntries), a.address); + GenerateBuildInputDefinitions(allEntries, bundleInputDefs, CalculateGroupHash(namingMode, assetGroup, allEntries), a.address, ignoreUnsupportedFilesInBuild); } } break; case BundledAssetGroupSchema.BundlePackingMode.PackTogetherByLabel: @@ -789,7 +793,7 @@ internal static List PrepGroupBundlePacking(AddressableAs a.GatherAllAssets(allEntries, true, true, false, entryFilter); } combinedEntries.AddRange(allEntries); - GenerateBuildInputDefinitions(allEntries, bundleInputDefs, CalculateGroupHash(namingMode, assetGroup, allEntries), entryGroup.Key); + GenerateBuildInputDefinitions(allEntries, bundleInputDefs, CalculateGroupHash(namingMode, assetGroup, allEntries), entryGroup.Key, ignoreUnsupportedFilesInBuild); } } break; default: @@ -798,12 +802,13 @@ internal static List PrepGroupBundlePacking(AddressableAs return combinedEntries; } - internal static void GenerateBuildInputDefinitions(List allEntries, List buildInputDefs, string groupGuid, string address) + internal static void GenerateBuildInputDefinitions(List allEntries, List buildInputDefs, string groupGuid, string address, bool ignoreUnsupportedFilesInBuild) { var scenes = new List(); var assets = new List(); foreach (var e in allEntries) { + ThrowExceptionIfInvalidFiletypeOrAddress(e, ignoreUnsupportedFilesInBuild); if (string.IsNullOrEmpty(e.AssetPath)) continue; if (e.IsScene) @@ -817,6 +822,19 @@ internal static void GenerateBuildInputDefinitions(List a buildInputDefs.Add(GenerateBuildInputDefinition(scenes, groupGuid + "_scenes_" + address + ".bundle")); } + private static void ThrowExceptionIfInvalidFiletypeOrAddress(AddressableAssetEntry entry, bool ignoreUnsupportedFilesInBuild) + { + if (entry.guid.Length > 0 && entry.address.Contains("[") && entry.address.Contains("]")) + throw new Exception($"Address '{entry.address}' cannot contain '[ ]'."); + if (entry.MainAssetType == typeof(DefaultAsset) && !AssetDatabase.IsValidFolder(entry.AssetPath)) + { + if (ignoreUnsupportedFilesInBuild) + Debug.LogWarning($"Cannot recognize file type for entry located at '{entry.AssetPath}'. Asset location will be ignored."); + else + throw new Exception($"Cannot recognize file type for entry located at '{entry.AssetPath}'. Asset import failed for using an unsupported file type."); + } + } + internal static AssetBundleBuild GenerateBuildInputDefinition(List assets, string name) { var assetInternalIds = new HashSet(); @@ -858,7 +876,7 @@ static string[] CreateRemoteCatalog(string jsonText, List var remoteHashLoadPath = remoteLoadFolder + versionedFileName + ".hash"; var remoteHashLoadLocation = new ResourceLocationData( - new[] {dependencyHashes[(int) ContentCatalogProvider.DependencyHashIndex.Remote]}, + new[] {dependencyHashes[(int)ContentCatalogProvider.DependencyHashIndex.Remote]}, remoteHashLoadPath, typeof(TextDataProvider), typeof(string)); remoteHashLoadLocation.Data = catalogLoadOptions.Copy(); @@ -866,7 +884,7 @@ static string[] CreateRemoteCatalog(string jsonText, List var cacheLoadPath = "{UnityEngine.Application.persistentDataPath}/com.unity.addressables" + versionedFileName + ".hash"; var cacheLoadLocation = new ResourceLocationData( - new[] {dependencyHashes[(int) ContentCatalogProvider.DependencyHashIndex.Cache]}, + new[] {dependencyHashes[(int)ContentCatalogProvider.DependencyHashIndex.Cache]}, cacheLoadPath, typeof(TextDataProvider), typeof(string)); cacheLoadLocation.Data = catalogLoadOptions.Copy(); @@ -968,6 +986,7 @@ void PostProcessBundles(AddressableAssetGroup assetGroup, List buildBund RetryCount = schema.RetryCount, Timeout = schema.Timeout, BundleName = Path.GetFileNameWithoutExtension(info.FileName), + AssetLoadMode = schema.AssetLoadMode, BundleSize = GetFileSize(info.FileName), ClearOtherCachedVersionsWhenLoaded = schema.AssetBundledCacheClearBehavior == BundledAssetGroupSchema.CacheClearBehavior.ClearWhenWhenNewVersionLoaded }; @@ -1033,7 +1052,6 @@ internal void AddPostCatalogUpdatesInternal(AddressableAssetGroup assetGroup, Li if (dataEntry != null) dataEntry.InternalId = StripHashFromBundleLocation(dataEntry.InternalId); - }); } } diff --git a/Editor/Build/DataBuilders/BuildScriptVirtualMode.cs b/Editor/Build/DataBuilders/BuildScriptVirtualMode.cs index 503cad7c..a5bfdcf2 100644 --- a/Editor/Build/DataBuilders/BuildScriptVirtualMode.cs +++ b/Editor/Build/DataBuilders/BuildScriptVirtualMode.cs @@ -223,6 +223,7 @@ TResult DoBuild(AddressablesDataBuilderInput builderInput, AddressableA RetryCount = schema.RetryCount, Timeout = schema.Timeout, BundleName = Path.GetFileName(bundleLocData.InternalId), + AssetLoadMode = schema.AssetLoadMode, BundleSize = dataSize + headerSize }; bundleLocData.Data = requestOptions; diff --git a/Editor/GUI/AddressableAssetSettingsInspector.cs b/Editor/GUI/AddressableAssetSettingsInspector.cs index 1a90d4b0..bd77b219 100644 --- a/Editor/GUI/AddressableAssetSettingsInspector.cs +++ b/Editor/GUI/AddressableAssetSettingsInspector.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using UnityEditor.AddressableAssets.Build; using UnityEditor.AddressableAssets.Settings; @@ -37,6 +38,10 @@ class AddressableAssetSettingsInspector : Editor [SerializeField] bool m_InitObjectsFoldout = true; + //Used for displaying path pairs + bool m_UseCustomPaths = false; + bool m_ShowPaths = true; + [FormerlySerializedAs("m_profileEntriesRL")] [SerializeField] ReorderableList m_ProfileEntriesRl; @@ -104,7 +109,7 @@ void OnEnable() new GUIContent("Non-Recursive Dependency Calculation", "If set, Calculates and build asset bundles using Non-Recursive Dependency calculation methods. This approach helps reduce asset bundle rebuilds and runtime memory consumption."); #else GUIContent m_NonRecursiveBundleBuilding = - new GUIContent("Non-Recursive Dependency Calculation", "If set, Calculates and build asset bundles using Non-Recursive Dependency calculation methods. This approach helps reduce asset bundle rebuilds and runtime memory consumption.\n*Requires Unity 2020.2.1 or above"); + new GUIContent("Non-Recursive Dependency Calculation", "If set, Calculates and build asset bundles using Non-Recursive Dependency calculation methods. This approach helps reduce asset bundle rebuilds and runtime memory consumption.\n*Requires Unity 2019.4.19f1 or above"); #endif GUIContent m_BuildRemoteCatalog = new GUIContent("Build Remote Catalog", "If set, this will create a copy of the content catalog for storage on a remote server. This catalog can be overwritten later for content updates."); @@ -241,8 +246,7 @@ public override void OnInspectorGUI() if ((m_AasTarget.RemoteCatalogBuildPath != null && m_AasTarget.RemoteCatalogLoadPath != null) // these will never actually be null, as the accessor initializes them. && (buildRemoteCatalog)) { - EditorGUILayout.PropertyField(serializedObject.FindProperty("m_RemoteCatalogBuildPath"), m_RemoteCatBuildPath); - EditorGUILayout.PropertyField(serializedObject.FindProperty("m_RemoteCatalogLoadPath"), m_RemoteCatLoadPath); + DrawRemoteCatalogPaths(); } } @@ -478,5 +482,80 @@ void OnAddInitializationObject(Rect buttonRect, ReorderableList list) } m_AasTarget.AddInitializationObject(initObj as IObjectInitializationDataProvider); } + + void DrawRemoteCatalogPaths() + { + ProfileValueReference BuildPath = m_AasTarget.RemoteCatalogBuildPath; + ProfileValueReference LoadPath = m_AasTarget.RemoteCatalogLoadPath; + + AddressableAssetSettings settings = AddressableAssetSettingsDefaultObject.Settings; + if (settings == null) return; + List groupTypes = ProfileGroupType.CreateGroupTypes(settings.profileSettings.GetProfile(settings.activeProfileId)); + List options = groupTypes.Select(group => group.GroupTypePrefix).ToList(); + //set selected to custom + options.Add(AddressableAssetProfileSettings.customEntryString); + int? selected = options.Count - 1; + HashSet vars = settings.profileSettings.GetAllVariableIds(); + if (vars.Contains(BuildPath.Id) && vars.Contains(LoadPath.Id) && !m_UseCustomPaths) + { + for (int i = 0; i < groupTypes.Count; i++) + { + ProfileGroupType.GroupTypeVariable buildPathVar = groupTypes[i].GetVariableBySuffix("BuildPath"); + ProfileGroupType.GroupTypeVariable loadPathVar = groupTypes[i].GetVariableBySuffix("LoadPath"); + if (BuildPath.GetName(settings) == groupTypes[i].GetName(buildPathVar) && LoadPath.GetName(settings) == groupTypes[i].GetName(loadPathVar)) + { + selected = i; + break; + } + } + } + + if (selected.HasValue && selected != options.Count - 1) + { + m_UseCustomPaths = false; + } + else + { + m_UseCustomPaths = true; + } + + EditorGUI.BeginChangeCheck(); + var newIndex = EditorGUILayout.Popup("Build & Load Paths", selected.HasValue ? selected.Value : options.Count - 1, options.ToArray()); + if (EditorGUI.EndChangeCheck() && newIndex != selected) + { + if (options[newIndex] != AddressableAssetProfileSettings.customEntryString) + { + Undo.RecordObject(serializedObject.targetObject, serializedObject.targetObject.name + "Path Pair"); + BuildPath.SetVariableByName(settings, groupTypes[newIndex].GroupTypePrefix + ProfileGroupType.k_PrefixSeparator + "BuildPath"); + LoadPath.SetVariableByName(settings, groupTypes[newIndex].GroupTypePrefix + ProfileGroupType.k_PrefixSeparator + "LoadPath"); + m_UseCustomPaths = false; + } + else + { + Undo.RecordObject(serializedObject.targetObject, serializedObject.targetObject.name + "Path Pair"); + m_UseCustomPaths = true; + } + EditorUtility.SetDirty(this); + } + + if (m_UseCustomPaths) + { + EditorGUILayout.PropertyField(serializedObject.FindProperty("m_RemoteCatalogBuildPath"), m_RemoteCatBuildPath); + EditorGUILayout.PropertyField(serializedObject.FindProperty("m_RemoteCatalogLoadPath"), m_RemoteCatLoadPath); + } + + EditorGUI.indentLevel++; + m_ShowPaths = EditorGUILayout.Foldout(m_ShowPaths, "Path Preview", true); + if (m_ShowPaths) + { + EditorStyles.helpBox.fontSize = 12; + var baseBuildPathValue = settings.profileSettings.GetValueById(settings.activeProfileId, BuildPath.Id); + var baseLoadPathValue = settings.profileSettings.GetValueById(settings.activeProfileId, LoadPath.Id); + EditorGUILayout.HelpBox(String.Format("Build Path: {0}", settings.profileSettings.EvaluateString(settings.activeProfileId, baseBuildPathValue)), MessageType.None); + EditorGUILayout.HelpBox(String.Format("Load Path: {0}", settings.profileSettings.EvaluateString(settings.activeProfileId, baseLoadPathValue)), MessageType.None); + } + EditorGUI.indentLevel--; + } + } } diff --git a/Editor/GUI/AddressableAssetsSettingsGroupTreeView.cs b/Editor/GUI/AddressableAssetsSettingsGroupTreeView.cs index 5d7b2440..cae5ecf4 100644 --- a/Editor/GUI/AddressableAssetsSettingsGroupTreeView.cs +++ b/Editor/GUI/AddressableAssetsSettingsGroupTreeView.cs @@ -174,12 +174,18 @@ protected IList Search(IList rows) return new List(); m_SearchedEntries.Clear(); - return rows.OfType() - .Where(row => ProjectConfigData.HierarchicalSearch - ? SearchHierarchical(row, customSearchString) - : DoesItemMatchSearch(row, searchString)) - .Cast() - .ToList(); + List items = new List(rows.Count); + foreach (TreeViewItem item in rows) + { + if (ProjectConfigData.HierarchicalSearch) + { + if(SearchHierarchical(item, customSearchString)) + items.Add(item); + } + else if (DoesItemMatchSearch(item, searchString)) + items.Add(item); + } + return items; } /* @@ -354,12 +360,16 @@ protected override bool DoesItemMatchSearch(TreeViewItem item, string search) //check if item matches. if (aeItem.displayName.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0) return true; - - if (aeItem.entry != null && aeItem.entry.AssetPath.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0) - return true; - if (aeItem.entry != null && m_Editor.settings.labelTable.GetString(aeItem.entry.labels, 200).IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0) + if (aeItem.entry == null) + return false; + if (aeItem.entry.AssetPath.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0) return true; - + + foreach (string label in aeItem.entry.labels) + { + if (label.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0) + return true; + } return false; } @@ -705,6 +715,9 @@ AssetEntryTreeViewItem FindItemInVisibleRows(int id) protected override void RenameEnded(RenameEndedArgs args) { + if (!args.acceptedRename) + return; + var item = FindItemInVisibleRows(args.itemID); if (item != null) { @@ -898,7 +911,13 @@ protected override void ContextClickedItem(int id) menu.AddItem(new GUIContent("Remove Addressables"), false, RemoveEntry, selectedNodes); menu.AddItem(new GUIContent("Simplify Addressable Names"), false, SimplifyAddresses, selectedNodes); - menu.AddItem(new GUIContent("Export Addressables"), false, CreateExternalEntryCollection, selectedNodes); + + if (selectedNodes.Count == 1) + menu.AddItem(new GUIContent("Copy Address to Clipboard"), false, CopyAddressesToClipboard, selectedNodes); + + else if (selectedNodes.Count > 1) + menu.AddItem(new GUIContent("Copy " + selectedNodes.Count + " Addresses to Clipboard"), false, CopyAddressesToClipboard, selectedNodes); + foreach (var i in AddressableAssetSettings.CustomAssetEntryCommands) menu.AddItem(new GUIContent(i), false, HandleCustomContextMenuItemEntries, new Tuple>(i, selectedNodes)); } @@ -925,6 +944,12 @@ protected override void ContextClickedItem(int id) menu.AddItem(new GUIContent("Move Addressables to group/" + g.Name), false, MoveEntriesToGroup, g); } } + + if (selectedNodes.Count == 1) + menu.AddItem(new GUIContent("Copy Address to Clipboard"), false, CopyAddressesToClipboard, selectedNodes); + + else if (selectedNodes.Count > 1) + menu.AddItem(new GUIContent("Copy " + selectedNodes.Count + " Addresses to Clipboard"), false, CopyAddressesToClipboard, selectedNodes); } } @@ -951,30 +976,19 @@ void GoToGroupAsset(object context) EditorGUIUtility.PingObject(group); Selection.activeObject = group; } - - void CreateExternalEntryCollection(object context) + + internal static void CopyAddressesToClipboard(object context) { - var path = EditorUtility.SaveFilePanel("Create Entry Collection", "Assets", "AddressableEntryCollection", "asset"); List selectedNodes = context as List; - - if (!string.IsNullOrEmpty(path) && selectedNodes != null) - { - var col = ScriptableObject.CreateInstance(); - foreach (var item in selectedNodes) - { - item.entry.ReadOnly = true; - item.entry.IsSubAsset = true; - col.Entries.Add(item.entry); - m_Editor.settings.RemoveAssetEntry(item.entry.guid, false); - } - path = path.Substring(path.ToLower().IndexOf("assets/")); - AssetDatabase.CreateAsset(col, path); - AssetDatabase.Refresh(); - var guid = AssetDatabase.AssetPathToGUID(path); - m_Editor.settings.CreateOrMoveEntry(guid, m_Editor.settings.DefaultGroup); - } - } - + string buffer = ""; + + foreach (AssetEntryTreeViewItem item in selectedNodes) + buffer += item.entry.address + ","; + + buffer = buffer.TrimEnd(','); + GUIUtility.systemCopyBuffer = buffer; + } + void MoveAllResourcesToGroup(object context) { var targetGroup = context as AddressableAssetGroup; @@ -1053,7 +1067,7 @@ internal void CreateNewGroup(object context) } } - void SetGroupAsDefault(object context) + internal void SetGroupAsDefault(object context) { List selectedNodes = context as List; if (selectedNodes == null || selectedNodes.Count == 0) @@ -1066,6 +1080,11 @@ void SetGroupAsDefault(object context) } protected void RemoveMissingReferences() + { + RemoveMissingReferencesImpl(); + } + + internal void RemoveMissingReferencesImpl() { if (m_Editor.settings.RemoveMissingGroupReferences()) m_Editor.settings.SetDirty(AddressableAssetSettings.ModificationEvent.GroupRemoved, null, true, true); @@ -1073,7 +1092,11 @@ protected void RemoveMissingReferences() protected void RemoveGroup(object context) { - if (EditorUtility.DisplayDialog("Delete selected groups?", "Are you sure you want to delete the selected groups?\n\nYou cannot undo this action.", "Yes", "No")) + RemoveGroupImpl(context); + } + internal void RemoveGroupImpl(object context, bool forceRemoval = false) + { + if (forceRemoval || EditorUtility.DisplayDialog("Delete selected groups?", "Are you sure you want to delete the selected groups?\n\nYou cannot undo this action.", "Yes", "No")) { List selectedNodes = context as List; if (selectedNodes == null || selectedNodes.Count < 1) @@ -1090,6 +1113,11 @@ protected void RemoveGroup(object context) } protected void SimplifyAddresses(object context) + { + SimplifyAddressesImpl(context); + } + + internal void SimplifyAddressesImpl(object context) { List selectedNodes = context as List; if (selectedNodes == null || selectedNodes.Count < 1) @@ -1124,7 +1152,12 @@ protected void SimplifyAddresses(object context) protected void RemoveEntry(object context) { - if (EditorUtility.DisplayDialog("Delete selected entries?", "Are you sure you want to delete the selected entries?\n\nYou cannot undo this action.", "Yes", "No")) + RemoveEntryImpl(context); + } + + internal void RemoveEntryImpl(object context, bool forceRemoval = false) + { + if (forceRemoval || EditorUtility.DisplayDialog("Delete selected entries?", "Are you sure you want to delete the selected entries?\n\nYou cannot undo this action.", "Yes", "No")) { List selectedNodes = context as List; if (selectedNodes == null || selectedNodes.Count < 1) @@ -1150,6 +1183,11 @@ protected void RemoveEntry(object context) } protected void RenameItem(object context) + { + RenameItemImpl(context); + } + + internal void RenameItemImpl(object context) { List selectedNodes = context as List; if (selectedNodes != null && selectedNodes.Count >= 1) diff --git a/Editor/GUI/AddressableAssetsSettingsLabelMaskPopup.cs b/Editor/GUI/AddressableAssetsSettingsLabelMaskPopup.cs index 063674ad..87682225 100644 --- a/Editor/GUI/AddressableAssetsSettingsLabelMaskPopup.cs +++ b/Editor/GUI/AddressableAssetsSettingsLabelMaskPopup.cs @@ -43,7 +43,7 @@ public override Vector2 GetWindowSize() var content = new GUIContent(maxStr); UnityEngine.GUI.skin.toggle.CalcMinMaxWidth(content, out minWidth, out maxWidth); var height = UnityEngine.GUI.skin.toggle.CalcHeight(content, maxWidth) + 3.5f; - m_Rect = new Vector2(Mathf.Clamp(maxWidth + 15, 125, 600), Mathf.Clamp(labelTable.labelNames.Count * height + 25, 30, 150)); + m_Rect = new Vector2(Mathf.Clamp(maxWidth + 35, 125, 600), Mathf.Clamp(labelTable.labelNames.Count * height + 25, 30, 150)); m_LastItemCount = labelTable.labelNames.Count; } return m_Rect; @@ -61,16 +61,23 @@ public override void OnGUI(Rect rect) if (m_Entries.Count == 0) return; - var labelTable = m_Settings.labelTable; - GUILayout.BeginArea(new Rect(rect.xMin + 3, rect.yMin + 3, rect.width - 6, rect.height - 6)); + var areaRect = new Rect(rect.xMin + 3, rect.yMin + 3, rect.width - 6, rect.height - 6); + GUILayout.BeginArea(areaRect); m_ScrollPosition = GUILayout.BeginScrollView(m_ScrollPosition, false, false); - - //string toRemove = null; + Vector2 yPositionDrawRange = new Vector2(m_ScrollPosition.y - 30, m_ScrollPosition.y + rect.height + 30); + foreach (var labelName in labelTable.labelNames) { - EditorGUILayout.BeginHorizontal(); + var toggleRect = EditorGUILayout.GetControlRect(GUILayout.Width(areaRect.width-20)); + if (toggleRect.height > 1) + { + // only draw toggles if they are in view + if (toggleRect.y < yPositionDrawRange.x || toggleRect.y > yPositionDrawRange.y) + continue; + } + else continue; bool newState; int count; @@ -80,30 +87,19 @@ public override void OnGUI(Rect rect) m_LabelCount.TryGetValue(labelName, out count); bool oldState = count == m_Entries.Count; - if (count == 0 || count == m_Entries.Count) - { - newState = GUILayout.Toggle(oldState, new GUIContent(labelName), GUILayout.ExpandWidth(false)); - } - else - { - if (m_ToggleMixed == null) - m_ToggleMixed = new GUIStyle("ToggleMixed"); - newState = GUILayout.Toggle(oldState, new GUIContent(labelName), m_ToggleMixed, GUILayout.ExpandWidth(false)); - } + if (!(count == 0 || count == m_Entries.Count)) + EditorGUI.showMixedValue = true; + newState = EditorGUI.ToggleLeft(toggleRect, new GUIContent(labelName), oldState); + EditorGUI.showMixedValue = false; + if (oldState != newState) - { SetLabelForEntries(labelName, newState); - } - - EditorGUILayout.EndHorizontal(); } - EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Manage Labels", UnityEngine.GUI.skin.button, GUILayout.ExpandWidth(false))) { EditorWindow.GetWindow(true).Intialize(m_Settings); } - EditorGUILayout.EndHorizontal(); GUILayout.EndScrollView(); GUILayout.EndArea(); diff --git a/Editor/GUI/CacheInitializationDataDrawer.cs b/Editor/GUI/CacheInitializationDataDrawer.cs index d5ddd06f..713c5a5c 100644 --- a/Editor/GUI/CacheInitializationDataDrawer.cs +++ b/Editor/GUI/CacheInitializationDataDrawer.cs @@ -54,7 +54,7 @@ internal float DrawExpirationDelayGUI(Rect rectForGUIRow, SerializedProperty pro if (!isPreview) { var prop = property.FindPropertyRelative("m_ExpirationDelay"); - prop.intValue = EditorGUI.IntSlider(rectForGUIRow, new GUIContent("Expiration Delay (in seconds)", "Controls how long items are left in the cache before deleting."), prop.intValue, 0, 12960000); + prop.intValue = EditorGUI.IntSlider(rectForGUIRow, new GUIContent("[Obsolete] Expiration Delay (in seconds)", "Controls how long items are left in the cache before deleting."), prop.intValue, 0, 12960000); rectForGUIRow.y += rectForGUIRow.height + EditorGUIUtility.standardVerticalSpacing; var ts = new TimeSpan(0, 0, prop.intValue); EditorGUI.LabelField(new Rect(rectForGUIRow.x + 16, rectForGUIRow.y, rectForGUIRow.width - 16, rectForGUIRow.height), new GUIContent(NicifyTimeSpan(ts))); diff --git a/Editor/GUI/ProfileTreeView.cs b/Editor/GUI/ProfileTreeView.cs index 43881c54..40c63838 100644 --- a/Editor/GUI/ProfileTreeView.cs +++ b/Editor/GUI/ProfileTreeView.cs @@ -222,6 +222,9 @@ protected void RenameProfile(object context) protected override void RenameEnded(RenameEndedArgs args) { + if (!args.acceptedRename) + return; + var item = FindItemInVisibleRows(args.itemID); AddressableAssetProfileSettings.BuildProfile profile = GetProfile(item.id); diff --git a/Editor/GUI/ProfileWindow.cs b/Editor/GUI/ProfileWindow.cs index 60c9ad9f..cf0f89f8 100644 --- a/Editor/GUI/ProfileWindow.cs +++ b/Editor/GUI/ProfileWindow.cs @@ -151,8 +151,9 @@ void TopToolbar(Rect toolbarPos) if (EditorGUI.DropdownButton(rMode, guiMode, FocusType.Passive, EditorStyles.toolbarDropDown)) { var menu = new GenericMenu(); - menu.AddItem(new GUIContent("New Profile"), false, NewProfile); - menu.AddItem(new GUIContent("New Variable (All Profiles)"), false, () => EditorApplication.delayCall += NewVariable); + menu.AddItem(new GUIContent("Profile"), false, NewProfile); + menu.AddItem(new GUIContent("Variable (All Profiles)"), false, () => EditorApplication.delayCall += NewVariable); + menu.AddItem(new GUIContent("Build & Load Path Variables (All Profiles)"), false, () => EditorApplication.delayCall += NewPathPair); menu.DropDown(rMode); } GUILayout.FlexibleSpace(); @@ -174,6 +175,19 @@ void NewVariable() } } + void NewPathPair() + { + try + { + PopupWindow.Show(new Rect(position.x, position.y + k_ToolbarHeight, position.width, k_ToolbarHeight), + new ProfileNewPathPairPopup(position.width, position.height, 0, m_ProfileTreeView, settings)); + } + catch (ExitGUIException) + { + // Exception not being caught through OnGUI call + } + } + //Contains all of the profile names, primarily implemented in ProfileTreeView void ProfilesPane(Rect profilesPaneRect) { @@ -212,7 +226,7 @@ void VariablesPane(Rect variablesPaneRect) } //ensures amount of visible text is not affected by label width - float fieldWidth = variablesPaneRect.width - (2 * k_ItemRectPadding) + m_LabelWidth + m_FieldBufferWidth; + float fieldWidth = variablesPaneRect.width - (2 * k_ItemRectPadding) + m_FieldBufferWidth; if (!EditorGUIUtility.labelWidth.Equals(m_LabelWidth)) EditorGUIUtility.labelWidth = m_LabelWidth; @@ -222,7 +236,7 @@ void VariablesPane(Rect variablesPaneRect) GUILayout.BeginArea(variablesPaneRect); EditorGUI.indentLevel++; - List groupTypes = CreateGroupTypes(selectedProfile); + List groupTypes = ProfileGroupType.CreateGroupTypes(selectedProfile); HashSet drawnGroupTypes = new HashSet(); //Displaying Path Groups @@ -247,7 +261,7 @@ void VariablesPane(Rect variablesPaneRect) foreach(var variable in pathVariables) { Rect newPathRect = EditorGUILayout.BeginVertical(); - string newPath = EditorGUILayout.TextField(groupType.GetName(variable), variable.Value); + string newPath = EditorGUILayout.TextField(groupType.GetName(variable), variable.Value, new GUILayoutOption[]{ GUILayout.Width(fieldWidth)}); EditorGUILayout.EndVertical(); if (evt.type == EventType.ContextClick) { @@ -272,7 +286,7 @@ void VariablesPane(Rect variablesPaneRect) { GUILayout.Space(5); Rect newValueRect = EditorGUILayout.BeginVertical(); - string newValue = EditorGUILayout.TextField(curVariable.ProfileName, selectedProfile.values[i].value); + string newValue = EditorGUILayout.TextField(curVariable.ProfileName, selectedProfile.values[i].value, new GUILayoutOption[] { GUILayout.Width(fieldWidth) }); EditorGUILayout.EndVertical(); if (newValue != selectedProfile.values[i].value && ProfileIndex == m_ProfileTreeView.lastClickedProfile) { @@ -298,34 +312,7 @@ void VariablesPane(Rect variablesPaneRect) m_FieldBufferWidth = Mathf.Clamp((maxFieldLen * k_ApproxCharWidth) - fieldWidth, 0f, float.MaxValue); } - //UI magic to group the path pairs from profile variables - List CreateGroupTypes(AddressableAssetProfileSettings.BuildProfile buildProfile) - { - Dictionary groups = new Dictionary(); - foreach(var profileEntry in settings.profileSettings.profileEntryNames) - { - string[] parts = profileEntry.ProfileName.Split(k_PrefixSeparator); - if (parts.Length > 1) - { - string prefix = String.Join(k_PrefixSeparator.ToString(), parts, 0, parts.Length - 1); - string suffix = parts[parts.Length - 1]; - string profileEntryValue = buildProfile.GetValueById(profileEntry.Id); - ProfileGroupType group; - groups.TryGetValue(prefix, out group); - if (group == null) - { - group = new ProfileGroupType(prefix); - } - ProfileGroupType.GroupTypeVariable variable = new ProfileGroupType.GroupTypeVariable(suffix, profileEntryValue); - group.AddVariable(variable); - groups[prefix] = group; - } - } - - List groupList = new List(); - groupList.AddRange(groups.Values.Where(group => group.IsValidGroupType())); - return groupList; - } + //Creates the context menu for the selected variable void CreateVariableContextMenu(Rect parentWindow, Rect menuRect, AddressableAssetProfileSettings.ProfileIdData variable, Event evt) @@ -518,5 +505,72 @@ public override void OnGUI(Rect windowRect) } } } + + class ProfileNewPathPairPopup : PopupWindowContent + { + internal float m_WindowWidth; + internal float m_WindowHeight; + internal float m_xOffset; + internal string m_Name; + internal string m_BuildPath; + internal string m_LoadPath; + internal bool m_NeedsFocus = true; + internal AddressableAssetSettings m_Settings; + + ProfileTreeView m_ProfileTreeView; + + public ProfileNewPathPairPopup(float width, float height, float xOffset, ProfileTreeView profileTreeView, AddressableAssetSettings settings) + { + m_WindowWidth = width; + m_WindowHeight = height; + m_xOffset = xOffset; + m_Settings = settings; + m_Name = m_Settings.profileSettings.GetUniqueProfileEntryName("New Entry"); + m_BuildPath = Application.dataPath; + m_LoadPath = Application.dataPath; + + m_ProfileTreeView = profileTreeView; + } + + public override Vector2 GetWindowSize() + { + float width = Mathf.Clamp(m_WindowWidth * 0.375f, Mathf.Min(600, m_WindowWidth - m_xOffset), m_WindowWidth); + float height = Mathf.Clamp(65, Mathf.Min(65, m_WindowHeight), m_WindowHeight); + return new Vector2(width, height); + } + + public override void OnGUI(Rect windowRect) + { + GUILayout.Space(5); + Event evt = Event.current; + bool hitEnter = evt.type == EventType.KeyDown && (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter); + EditorGUIUtility.labelWidth = 120; + m_Name = EditorGUILayout.TextField("Prefix Name", m_Name); + m_BuildPath = EditorGUILayout.TextField("Build Path Value", m_BuildPath); + m_LoadPath = EditorGUILayout.TextField("Load Path Value", m_LoadPath); + + UnityEngine.GUI.enabled = m_Name.Length != 0; + if (GUILayout.Button("Save") || hitEnter) + { + string buildPathName = m_Name + ProfileGroupType.k_PrefixSeparator + "BuildPath"; + string loadPathName = m_Name + ProfileGroupType.k_PrefixSeparator + "LoadPath"; + if (string.IsNullOrEmpty(m_Name)) + Debug.LogError("Variable name cannot be empty."); + else if (buildPathName != m_Settings.profileSettings.GetUniqueProfileEntryName(buildPathName)) + Debug.LogError("Profile variable '" + buildPathName + "' already exists."); + else if (loadPathName != m_Settings.profileSettings.GetUniqueProfileEntryName(loadPathName)) + Debug.LogError("Profile variable '" + loadPathName + "' already exists."); + else + { + Undo.RecordObject(m_Settings, "Profile Path Pair Created"); + m_Settings.profileSettings.CreateValue(m_Name + ProfileGroupType.k_PrefixSeparator + "BuildPath", m_BuildPath); + m_Settings.profileSettings.CreateValue(m_Name + ProfileGroupType.k_PrefixSeparator + "LoadPath", m_LoadPath); + AddressableAssetUtility.OpenAssetIfUsingVCIntegration(m_Settings); + m_ProfileTreeView.Reload(); + editorWindow.Close(); + } + } + } + } } } diff --git a/Editor/Settings/AddressableAssetProfileSettings.cs b/Editor/Settings/AddressableAssetProfileSettings.cs index 1e45b3d3..44b7a659 100644 --- a/Editor/Settings/AddressableAssetProfileSettings.cs +++ b/Editor/Settings/AddressableAssetProfileSettings.cs @@ -587,7 +587,7 @@ internal BuildProfile GetProfile(string profileId) return m_Profiles.Find(p => p.id == profileId); } - string GetVariableId(string variableName) + internal string GetVariableId(string variableName) { foreach (var idPair in profileEntryNames) { diff --git a/Editor/Settings/AddressableAssetSettings.cs b/Editor/Settings/AddressableAssetSettings.cs index 4d8b33e6..710618af 100644 --- a/Editor/Settings/AddressableAssetSettings.cs +++ b/Editor/Settings/AddressableAssetSettings.cs @@ -72,7 +72,7 @@ private static void TryAddAssetPostprocessorOnNextUpdate() public const string kRemoteLoadPath = "RemoteLoadPath"; /// - /// Options for labelling all the different generated events. + /// Options for labeling all the different generated events. /// public enum ModificationEvent { @@ -425,6 +425,7 @@ public bool DisableCatalogUpdateOnStartup } #if UNITY_2019_4_OR_NEWER + [SerializeField] bool m_StripUnityVersionFromBundleBuild = false; /// /// If true, this option will strip the Unity Editor Version from the header of the AssetBundle during a build. @@ -435,6 +436,7 @@ internal bool StripUnityVersionFromBundleBuild set { m_StripUnityVersionFromBundleBuild = value; } } #endif + [SerializeField] bool m_DisableVisibleSubAssetRepresentations = false; /// /// If true, the build will assume that sub Assets have no visible asset representations (are not visible in the Project view) which results in improved build times. diff --git a/Editor/Settings/GroupSchemas/BundledAssetGroupSchema.cs b/Editor/Settings/GroupSchemas/BundledAssetGroupSchema.cs index 2656f03d..1b9cd982 100644 --- a/Editor/Settings/GroupSchemas/BundledAssetGroupSchema.cs +++ b/Editor/Settings/GroupSchemas/BundledAssetGroupSchema.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Collections.Generic; using System.ComponentModel; using UnityEditor.AddressableAssets.HostingServices; @@ -6,6 +7,7 @@ using UnityEngine.ResourceManagement.ResourceProviders; using UnityEngine.ResourceManagement.Util; using UnityEngine.Serialization; +using UnityEngine.AddressableAssets; namespace UnityEditor.AddressableAssets.Settings.GroupSchemas { @@ -513,6 +515,20 @@ public string HostingServicesContentRoot /// public SerializedType AssetBundleProviderType { get { return m_AssetBundleProviderType; } } + /// + /// Used to determine if dropdown should be custom + /// + private bool m_UseCustomPaths = false; + + + /// + /// Internal settings + /// + internal AddressableAssetSettings settings + { + get { return AddressableAssetSettingsDefaultObject.Settings; } + } + /// /// Set default values taken from the assigned group. /// @@ -675,6 +691,21 @@ public BundleNamingStyle BundleNaming SetDirty(true); } } + + [SerializeField] + AssetLoadMode m_AssetLoadMode; + /// + /// Will load all Assets into memory from the AssetBundle after the AssetBundle is loaded. + /// + internal AssetLoadMode AssetLoadMode + { + get { return m_AssetLoadMode; } + set + { + m_AssetLoadMode = value; + SetDirty(true); + } + } private bool m_ShowPaths = true; private bool m_ShowAdvanced = false; @@ -693,11 +724,7 @@ public override void OnGUI() { var so = new SerializedObject(this); - m_ShowPaths = EditorGUILayout.Foldout(m_ShowPaths, "Build and Load Paths"); - if (m_ShowPaths) - { - ShowPaths(so); - } + ShowSelectedPropertyPathPair(so); m_ShowAdvanced = EditorGUILayout.Foldout(m_ShowAdvanced, "Advanced Options"); if (m_ShowAdvanced) @@ -717,17 +744,10 @@ public override void OnGUIMultiple(List otherSchema otherBundledSchemas.Add(schema as BundledAssetGroupSchema); } - EditorGUI.BeginChangeCheck(); - m_ShowPaths = EditorGUILayout.Foldout(m_ShowPaths, "Build and Load Paths"); - if (EditorGUI.EndChangeCheck()) - { - foreach (var schema in otherBundledSchemas) + foreach (var schema in otherBundledSchemas) schema.m_ShowPaths = m_ShowPaths; - } - if (m_ShowPaths) - { - ShowPathsMulti(so, otherSchemas, ref queuedChanges); - } + ShowSelectedPropertyPathPairMulti(so, otherSchemas, ref queuedChanges, + (src, dst) => { dst.m_BuildPath.Id = src.BuildPath.Id; dst.m_LoadPath.Id = src.LoadPath.Id; dst.m_UseCustomPaths = src.m_UseCustomPaths; dst.SetDirty(true); }); EditorGUI.BeginChangeCheck(); m_ShowAdvanced = EditorGUILayout.Foldout(m_ShowAdvanced, "Advanced Options"); @@ -789,6 +809,9 @@ void ShowPathsMulti(SerializedObject so, List other GUIContent m_CacheClearBehaviorContent = new GUIContent("Cache Clear Behavior", "Controls how old cached asset bundles are cleared."); GUIContent m_BundleModeContent = new GUIContent("Bundle Mode", "Controls how bundles are created from this group."); GUIContent m_BundleNamingContent = new GUIContent("Bundle Naming Mode", "Controls the final file naming mode for bundles in this group."); + GUIContent m_AssetLoadModeContent = new GUIContent("Asset Load Mode", "Determines how Assets are loaded when accessed." + + "\n- Requested Asset And Dependencies, will only load the requested Asset (Recommended)." + + "\n- All Packed Assets And Dependencies, will load all Assets that are packed together. Best used when loading all Assets into memory is required."); GUIContent m_AssetProviderContent = new GUIContent("Asset Provider", "The provider to use for loading assets out of AssetBundles"); GUIContent m_BundleProviderContent = new GUIContent("Asset Bundle Provider", "The provider to use for loading AssetBundles (not the assets within bundles)"); @@ -814,6 +837,7 @@ void ShowAdvancedProperties(SerializedObject so) EditorGUILayout.PropertyField(so.FindProperty(nameof(m_CacheClearBehavior)), m_CacheClearBehaviorContent, true); EditorGUILayout.PropertyField(so.FindProperty(nameof(m_BundleMode)), m_BundleModeContent, true); EditorGUILayout.PropertyField(so.FindProperty(nameof(m_BundleNaming)), m_BundleNamingContent, true); + EditorGUILayout.PropertyField(so.FindProperty(nameof(m_AssetLoadMode)), m_AssetLoadModeContent, true); EditorGUILayout.PropertyField(so.FindProperty(nameof(m_BundledAssetProviderType)), m_AssetProviderContent, true); EditorGUILayout.PropertyField(so.FindProperty(nameof(m_AssetBundleProviderType)), m_BundleProviderContent, true); } @@ -839,6 +863,7 @@ void ShowAdvancedPropertiesMulti(SerializedObject so, List dst.AssetBundledCacheClearBehavior = src.AssetBundledCacheClearBehavior, ref m_CacheClearBehavior); ShowSelectedPropertyMulti(so, nameof(m_BundleMode), m_BundleModeContent, otherBundledSchemas, ref queuedChanges, (src, dst) => dst.BundleMode = src.BundleMode, ref m_BundleMode); ShowSelectedPropertyMulti(so, nameof(m_BundleNaming), m_BundleNamingContent, otherBundledSchemas, ref queuedChanges, (src, dst) => dst.BundleNaming = src.BundleNaming, ref m_BundleNaming); + ShowSelectedPropertyMulti(so, nameof(m_AssetLoadMode), m_AssetLoadModeContent, otherBundledSchemas, ref queuedChanges, (src, dst) => dst.AssetLoadMode = src.AssetLoadMode, ref m_AssetLoadMode); ShowSelectedPropertyMulti(so, nameof(m_BundledAssetProviderType), m_AssetProviderContent, otherBundledSchemas, ref queuedChanges, (src, dst) => { dst.m_BundledAssetProviderType = src.BundledAssetProviderType; dst.SetDirty(true); }, ref m_BundledAssetProviderType); ShowSelectedPropertyMulti(so, nameof(m_AssetBundleProviderType), m_BundleProviderContent, otherBundledSchemas, ref queuedChanges, (src, dst) => { dst.m_AssetBundleProviderType = src.AssetBundleProviderType; dst.SetDirty(true); }, ref m_AssetBundleProviderType); } @@ -910,6 +935,7 @@ void ShowSelectedPropertyPath(SerializedObject so, string propertyName, GUIConte var prop = so.FindProperty(propertyName); string previousValue = currentValue.Id; EditorGUI.BeginChangeCheck(); + //Current implementation using ProfileValueReferenceDrawer EditorGUILayout.PropertyField(prop, label, true); if (EditorGUI.EndChangeCheck()) { @@ -921,5 +947,147 @@ void ShowSelectedPropertyPath(SerializedObject so, string propertyName, GUIConte } EditorGUI.showMixedValue = false; } + + void ShowSelectedPropertyPathPairMulti(SerializedObject so, List otherSchemas, ref List> queuedChanges, + Action a) + { + var buildPathProperty = so.FindProperty(nameof(m_BuildPath)); + var loadPathProperty = so.FindProperty(nameof(m_LoadPath)); + ShowMixedValue(buildPathProperty, otherSchemas, typeof(ProfileValueReference), nameof(m_BuildPath)); + ShowMixedValue(loadPathProperty, otherSchemas, typeof(ProfileValueReference), nameof(m_LoadPath)); + + List groupTypes = ProfileGroupType.CreateGroupTypes(settings.profileSettings.GetProfile(settings.activeProfileId)); + List options = groupTypes.Select(group => group.GroupTypePrefix).ToList(); + //set selected to custom + options.Add(AddressableAssetProfileSettings.customEntryString); + int? selected = null; + + //Determine selection and whether to show custom + if (!EditorGUI.showMixedValue) + { + //disregard custom value, want to check if valid pair + selected = DetermineSelectedIndex(groupTypes, options.Count - 1); + if (selected.HasValue && selected != options.Count - 1) + { + m_UseCustomPaths = false; + } + else + { + m_UseCustomPaths = true; + } + } + + //Dropdown selector + EditorGUI.BeginChangeCheck(); + var newIndex = EditorGUILayout.Popup("Build & Load Paths", selected.HasValue? selected.Value : -1, options.ToArray()); + if (EditorGUI.EndChangeCheck() && newIndex != selected) + { + selected = newIndex; + SetPathPairOption(so, options, groupTypes, newIndex); + + if (queuedChanges == null) + queuedChanges = new List>(); + queuedChanges.Add(a); + EditorGUI.showMixedValue = false; + } + + if (m_UseCustomPaths && selected.HasValue) + { + ShowPathsMulti(so, otherSchemas, ref queuedChanges); + } + + ShowPathsPreview(!selected.HasValue); + EditorGUI.showMixedValue = false; + } + + void ShowSelectedPropertyPathPair(SerializedObject so) + { + List groupTypes = ProfileGroupType.CreateGroupTypes(settings.profileSettings.GetProfile(settings.activeProfileId)); + List options = groupTypes.Select(group => group.GroupTypePrefix).ToList(); + //Set selected to custom + options.Add(AddressableAssetProfileSettings.customEntryString); + int? selected = options.Count - 1; + + //Determine selection and whether to show custom + selected = DetermineSelectedIndex(groupTypes, options.Count - 1); + if (selected.HasValue && selected != options.Count - 1) + { + m_UseCustomPaths = false; + } + else + { + m_UseCustomPaths = true; + } + + //Dropdown selector + EditorGUI.BeginChangeCheck(); + var newIndex = EditorGUILayout.Popup("Build & Load Paths", selected.HasValue? selected.Value : options.Count - 1, options.ToArray()); + if (EditorGUI.EndChangeCheck() && newIndex != selected) + { + SetPathPairOption(so, options, groupTypes, newIndex); + EditorUtility.SetDirty(this); + } + + if (m_UseCustomPaths) + { + ShowPaths(so); + } + + ShowPathsPreview(false); + EditorGUI.showMixedValue = false; + } + + int? DetermineSelectedIndex(List groupTypes, int? defaultValue) + { + int? selected = defaultValue; + + HashSet vars = settings.profileSettings.GetAllVariableIds(); + if (vars.Contains(m_BuildPath.Id) && vars.Contains(m_LoadPath.Id) && !m_UseCustomPaths) + { + for (int i = 0; i < groupTypes.Count; i++) + { + ProfileGroupType.GroupTypeVariable buildPathVar = groupTypes[i].GetVariableBySuffix("BuildPath"); + ProfileGroupType.GroupTypeVariable loadPathVar = groupTypes[i].GetVariableBySuffix("LoadPath"); + if (m_BuildPath.GetName(settings) == groupTypes[i].GetName(buildPathVar) && m_LoadPath.GetName(settings) == groupTypes[i].GetName(loadPathVar)) + { + selected = i; + break; + } + } + } + return selected; + } + + void SetPathPairOption(SerializedObject so, List options, List groupTypes, int newIndex) + { + + if (options[newIndex] != AddressableAssetProfileSettings.customEntryString) + { + Undo.RecordObject(so.targetObject, so.targetObject.name + "Path Pair"); + m_BuildPath.SetVariableByName(settings, groupTypes[newIndex].GroupTypePrefix + ProfileGroupType.k_PrefixSeparator + "BuildPath"); + m_LoadPath.SetVariableByName(settings, groupTypes[newIndex].GroupTypePrefix + ProfileGroupType.k_PrefixSeparator + "LoadPath"); + m_UseCustomPaths = false; + } + else + { + Undo.RecordObject(so.targetObject, so.targetObject.name + "Path Pair"); + m_UseCustomPaths = true; + } + } + + void ShowPathsPreview(bool showMixedValue) + { + EditorGUI.indentLevel++; + m_ShowPaths = EditorGUILayout.Foldout(m_ShowPaths, "Path Preview", true); + if (m_ShowPaths) + { + EditorStyles.helpBox.fontSize = 12; + var baseBuildPathValue = settings.profileSettings.GetValueById(settings.activeProfileId, m_BuildPath.Id); + var baseLoadPathValue = settings.profileSettings.GetValueById(settings.activeProfileId, m_LoadPath.Id); + EditorGUILayout.HelpBox(String.Format("Build Path: {0}", showMixedValue ? "-" : settings.profileSettings.EvaluateString(settings.activeProfileId, baseBuildPathValue)), MessageType.None); + EditorGUILayout.HelpBox(String.Format("Load Path: {0}", showMixedValue ? "-" : settings.profileSettings.EvaluateString(settings.activeProfileId, baseLoadPathValue)), MessageType.None); + } + EditorGUI.indentLevel--; + } } } diff --git a/Editor/Settings/LabelTable.cs b/Editor/Settings/LabelTable.cs index 59075ec4..7d11413b 100644 --- a/Editor/Settings/LabelTable.cs +++ b/Editor/Settings/LabelTable.cs @@ -63,6 +63,9 @@ internal bool RemoveLabelName(string name) internal string GetString(HashSet val, float width) //TODO - use width to add the "..." in the right place. { + if (val == null || val.Count == 0) + return ""; + StringBuilder sb = new StringBuilder(); int counter = 0; foreach (var v in m_LabelNames) diff --git a/Editor/Settings/ProfileGroupType.cs b/Editor/Settings/ProfileGroupType.cs index 7549b866..11bbd7fe 100644 --- a/Editor/Settings/ProfileGroupType.cs +++ b/Editor/Settings/ProfileGroupType.cs @@ -48,7 +48,7 @@ internal string Value } } - const char k_PrefixSeparator = '.'; + internal const char k_PrefixSeparator = '.'; string m_GroupTypePrefix; @@ -139,6 +139,36 @@ internal GroupTypeVariable GetVariableBySuffix(string suffix) return m_Variables.Where(var => var.m_Suffix == suffix).FirstOrDefault(); } + //UI magic to group the path pairs from profile variables + internal static List CreateGroupTypes(AddressableAssetProfileSettings.BuildProfile buildProfile) + { + AddressableAssetSettings settings = AddressableAssetSettingsDefaultObject.Settings; + Dictionary groups = new Dictionary(); + foreach (var profileEntry in settings.profileSettings.profileEntryNames) + { + string[] parts = profileEntry.ProfileName.Split(k_PrefixSeparator); + if (parts.Length > 1) + { + string prefix = String.Join(k_PrefixSeparator.ToString(), parts, 0, parts.Length - 1); + string suffix = parts[parts.Length - 1]; + string profileEntryValue = buildProfile.GetValueById(profileEntry.Id); + ProfileGroupType group; + groups.TryGetValue(prefix, out group); + if (group == null) + { + group = new ProfileGroupType(prefix); + } + ProfileGroupType.GroupTypeVariable variable = new ProfileGroupType.GroupTypeVariable(suffix, profileEntryValue); + group.AddVariable(variable); + groups[prefix] = group; + } + } + + List groupList = new List(); + groupList.AddRange(groups.Values.Where(group => group.IsValidGroupType())); + return groupList; + } + /// /// Determines if the group type is a valid /// diff --git a/Editor/Unity.Addressables.Editor.asmdef b/Editor/Unity.Addressables.Editor.asmdef index 0450ce35..db8b03bb 100644 --- a/Editor/Unity.Addressables.Editor.asmdef +++ b/Editor/Unity.Addressables.Editor.asmdef @@ -15,7 +15,7 @@ "versionDefines": [ { "name": "Unity", - "expression": "2020.2.0a9", + "expression": "2019.4.19f1", "define": "NONRECURSIVE_DEPENDENCY_DATA" } ] diff --git a/Runtime/Initialization/CacheInitialization.cs b/Runtime/Initialization/CacheInitialization.cs index bf73c76f..1bdf6795 100644 --- a/Runtime/Initialization/CacheInitialization.cs +++ b/Runtime/Initialization/CacheInitialization.cs @@ -45,8 +45,9 @@ public bool Initialize(string id, string dataStr) activeCache.maximumAvailableStorageSpace = data.MaximumCacheSize; else activeCache.maximumAvailableStorageSpace = long.MaxValue; - +#pragma warning disable 618 activeCache.expirationDelay = data.ExpirationDelay; +#pragma warning restore 618 } #endif //ENABLE_CACHING return true; @@ -148,6 +149,7 @@ public class CacheInitializationData /// /// Controls how long bundles are kept in the cache. This value is applied to Caching.currentCacheForWriting.expirationDelay. The value is in seconds and has a limit of 12960000 (150 days). /// + [Obsolete("Functionality remains unchanged. However, due to issues with Caching this property is being marked obsolete. See Caching API documentation for more details.")] public int ExpirationDelay { get { return m_ExpirationDelay; } set { m_ExpirationDelay = value; } } [FormerlySerializedAs("m_limitCacheSize")] diff --git a/Runtime/ResourceManager/ResourceProviders/AssetBundleProvider.cs b/Runtime/ResourceManager/ResourceProviders/AssetBundleProvider.cs index b6e7ff81..45749909 100644 --- a/Runtime/ResourceManager/ResourceProviders/AssetBundleProvider.cs +++ b/Runtime/ResourceManager/ResourceProviders/AssetBundleProvider.cs @@ -12,6 +12,15 @@ namespace UnityEngine.ResourceManagement.ResourceProviders { + /// + /// Used to indication how Assets are loaded from the AssetBundle on the first load request. + /// + internal enum AssetLoadMode + { + RequestedAssetAndDependencies = 0, + AllPackedAssetsAndDependencies, + } + /// /// Wrapper for asset bundles. /// @@ -79,6 +88,17 @@ public class AssetBundleRequestOptions : ILocationSizeData /// The name of the original bundle. This does not contain the appended hash. /// public string BundleName { get { return m_BundleName; } set { m_BundleName = value; } } + + [SerializeField] + AssetLoadMode m_AssetLoadMode = AssetLoadMode.RequestedAssetAndDependencies; + /// + /// Determines how Assets are loaded when accessed. + /// + /// + /// Requested Asset And Dependencies, will only load the requested Asset (Recommended). + /// All Packed Assets And Dependencies, will load all Assets that are packed together. Best used when loading all Assets into memory is required. + /// + internal AssetLoadMode AssetLoadMode { get { return m_AssetLoadMode; } set { m_AssetLoadMode = value; } } [SerializeField] long m_BundleSize; @@ -155,6 +175,23 @@ internal enum LoadType bool m_Completed = false; const int k_WaitForWebRequestMainThreadSleep = 1; string m_TransformedInternalId; + AssetBundleRequest m_PreloadRequest; + bool m_PreloadCompleted = false; + + internal long BytesToDownload + { + get + { + if (m_BytesToDownload == -1) + { + if (m_Options != null) + m_BytesToDownload = m_Options.ComputeSize(m_ProvideHandle.Location, m_ProvideHandle.ResourceManager); + else + m_BytesToDownload = 0; + } + return m_BytesToDownload; + } + } internal UnityWebRequest CreateWebRequest(IResourceLocation loc) { @@ -198,6 +235,32 @@ internal UnityWebRequest CreateWebRequest(string url) m_ProvideHandle.ResourceManager.WebRequestOverride?.Invoke(webRequest); return webRequest; } + + internal AssetBundleRequest GetAssetPreloadRequest() + { + if (m_PreloadCompleted || GetAssetBundle() == null) + return null; + + if (m_Options.AssetLoadMode == AssetLoadMode.AllPackedAssetsAndDependencies) + { +#if !UNITY_2021_1_OR_NEWER + if (AsyncOperationHandle.IsWaitingForCompletion) + { + m_AssetBundle.LoadAllAssets(); + m_PreloadCompleted = true; + return null; + } +#endif + if (m_PreloadRequest == null) + { + m_PreloadRequest = m_AssetBundle.LoadAllAssetsAsync(); + m_PreloadRequest.completed += operation => m_PreloadCompleted = true; + } + return m_PreloadRequest; + } + + return null; + } float PercentComplete() { return m_RequestOperation != null ? m_RequestOperation.progress : 0.0f; } @@ -205,8 +268,8 @@ DownloadStatus GetDownloadStatus() { if (m_Options == null) return default; - var status = new DownloadStatus() { TotalBytes = m_BytesToDownload, IsDone = PercentComplete() >= 1f }; - if (m_BytesToDownload > 0) + var status = new DownloadStatus() { TotalBytes = BytesToDownload, IsDone = PercentComplete() >= 1f }; + if (BytesToDownload > 0) { if (m_WebRequestQueueOperation != null && string.IsNullOrEmpty(m_WebRequestQueueOperation.m_WebRequest.error)) m_DownloadedBytes = (long)(m_WebRequestQueueOperation.m_WebRequest.downloadedBytes); @@ -249,8 +312,7 @@ internal void Start(ProvideHandle provideHandle) m_WebRequestCompletedCallbackCalled = false; m_ProvideHandle = provideHandle; m_Options = m_ProvideHandle.Location.Data as AssetBundleRequestOptions; - if (m_Options != null) - m_BytesToDownload = m_Options.ComputeSize(m_ProvideHandle.Location, m_ProvideHandle.ResourceManager); + m_BytesToDownload = -1; m_ProvideHandle.SetProgressCallback(PercentComplete); m_ProvideHandle.SetDownloadProgressCallbacks(GetDownloadStatus); m_ProvideHandle.SetWaitForCompletionCallback(WaitForCompletionHandler); diff --git a/Runtime/ResourceManager/ResourceProviders/BundledAssetProvider.cs b/Runtime/ResourceManager/ResourceProviders/BundledAssetProvider.cs index 8c56ea21..eb62ec74 100644 --- a/Runtime/ResourceManager/ResourceProviders/BundledAssetProvider.cs +++ b/Runtime/ResourceManager/ResourceProviders/BundledAssetProvider.cs @@ -14,21 +14,23 @@ public class BundledAssetProvider : ResourceProviderBase { internal class InternalOp { + AssetBundle m_AssetBundle; + AssetBundleRequest m_PreloadRequest; AssetBundleRequest m_RequestOperation; object m_Result; ProvideHandle m_ProvideHandle; string subObjectName = null; - internal static IAssetBundleResource LoadBundleFromDependecies(IList results) + internal static AssetBundleResource LoadBundleFromDependecies(IList results) { if (results == null || results.Count == 0) return null; - IAssetBundleResource bundle = null; + AssetBundleResource bundle = null; bool firstBundleWrapper = true; for (int i = 0; i < results.Count; i++) { - var abWrapper = results[i] as IAssetBundleResource; + var abWrapper = results[i] as AssetBundleResource; if (abWrapper != null) { //only use the first asset bundle, even if it is invalid @@ -57,80 +59,97 @@ public void Start(ProvideHandle provideHandle) } else { - var bundle = bundleResource.GetAssetBundle(); - if (bundle == null) + m_AssetBundle = bundleResource.GetAssetBundle(); + if (m_AssetBundle == null) { m_ProvideHandle.Complete(null, false, new Exception("Unable to load dependent bundle from location " + m_ProvideHandle.Location)); + return; } + + m_PreloadRequest = bundleResource.GetAssetPreloadRequest(); + if (m_PreloadRequest == null || m_PreloadRequest.isDone) + BeginAssetLoad(); else + m_PreloadRequest.completed += operation => BeginAssetLoad(); + } + } + + private void BeginAssetLoad() + { + if (m_AssetBundle == null) + { + m_ProvideHandle.Complete(null, false, new Exception("Unable to load dependent bundle from location " + m_ProvideHandle.Location)); + } + else + { + var assetPath = m_ProvideHandle.ResourceManager.TransformInternalId(m_ProvideHandle.Location); + if (m_ProvideHandle.Type.IsArray) { - var assetPath = m_ProvideHandle.ResourceManager.TransformInternalId(m_ProvideHandle.Location); - if (m_ProvideHandle.Type.IsArray) - { #if !UNITY_2021_1_OR_NEWER - if (AsyncOperationHandle.IsWaitingForCompletion) - { - GetArrayResult(bundle.LoadAssetWithSubAssets(assetPath, m_ProvideHandle.Type.GetElementType())); - CompleteOperation(); - } - else + if (AsyncOperationHandle.IsWaitingForCompletion) + { + GetArrayResult(m_AssetBundle.LoadAssetWithSubAssets(assetPath, m_ProvideHandle.Type.GetElementType())); + CompleteOperation(); + } + else #endif - m_RequestOperation = bundle.LoadAssetWithSubAssetsAsync(assetPath, m_ProvideHandle.Type.GetElementType()); + m_RequestOperation = m_AssetBundle.LoadAssetWithSubAssetsAsync(assetPath, m_ProvideHandle.Type.GetElementType()); + } + else if (m_ProvideHandle.Type.IsGenericType && typeof(IList<>) == m_ProvideHandle.Type.GetGenericTypeDefinition()) + { +#if !UNITY_2021_1_OR_NEWER + if (AsyncOperationHandle.IsWaitingForCompletion) + { + GetListResult(m_AssetBundle.LoadAssetWithSubAssets(assetPath, m_ProvideHandle.Type.GetGenericArguments()[0])); + CompleteOperation(); } - else if (m_ProvideHandle.Type.IsGenericType && typeof(IList<>) == m_ProvideHandle.Type.GetGenericTypeDefinition()) + else +#endif + m_RequestOperation = m_AssetBundle.LoadAssetWithSubAssetsAsync(assetPath, m_ProvideHandle.Type.GetGenericArguments()[0]); + } + else + { + if (ResourceManagerConfig.ExtractKeyAndSubKey(assetPath, out string mainPath, out string subKey)) { + subObjectName = subKey; #if !UNITY_2021_1_OR_NEWER if (AsyncOperationHandle.IsWaitingForCompletion) { - GetListResult(bundle.LoadAssetWithSubAssets(assetPath, m_ProvideHandle.Type.GetGenericArguments()[0])); + GetAssetSubObjectResult(m_AssetBundle.LoadAssetWithSubAssets(mainPath, m_ProvideHandle.Type)); CompleteOperation(); } else #endif - m_RequestOperation = bundle.LoadAssetWithSubAssetsAsync(assetPath, m_ProvideHandle.Type.GetGenericArguments()[0]); + m_RequestOperation = m_AssetBundle.LoadAssetWithSubAssetsAsync(mainPath, m_ProvideHandle.Type); } else { - if (ResourceManagerConfig.ExtractKeyAndSubKey(assetPath, out string mainPath, out string subKey)) - { - subObjectName = subKey; #if !UNITY_2021_1_OR_NEWER - if (AsyncOperationHandle.IsWaitingForCompletion) - { - GetAssetSubObjectResult(bundle.LoadAssetWithSubAssets(mainPath, m_ProvideHandle.Type)); - CompleteOperation(); - } - else -#endif - m_RequestOperation = bundle.LoadAssetWithSubAssetsAsync(mainPath, m_ProvideHandle.Type); - } - else + if (AsyncOperationHandle.IsWaitingForCompletion) { -#if !UNITY_2021_1_OR_NEWER - if (AsyncOperationHandle.IsWaitingForCompletion) - { - GetAssetResult(bundle.LoadAsset(assetPath, m_ProvideHandle.Type)); - CompleteOperation(); - } - else -#endif - m_RequestOperation = bundle.LoadAssetAsync(assetPath, m_ProvideHandle.Type); + GetAssetResult(m_AssetBundle.LoadAsset(assetPath, m_ProvideHandle.Type)); + CompleteOperation(); } - } - - if (m_RequestOperation != null) - { - if (m_RequestOperation.isDone) - ActionComplete(m_RequestOperation); else - m_RequestOperation.completed += ActionComplete; +#endif + m_RequestOperation = m_AssetBundle.LoadAssetAsync(assetPath, m_ProvideHandle.Type); } } + + if (m_RequestOperation != null) + { + if (m_RequestOperation.isDone) + ActionComplete(m_RequestOperation); + else + m_RequestOperation.completed += ActionComplete; + } } } private bool WaitForCompletionHandler() { + if (m_PreloadRequest != null && !m_PreloadRequest.isDone) + return m_PreloadRequest.asset == null; if (m_Result != null) return true; if (m_RequestOperation == null) diff --git a/Runtime/ResourceManager/ResourceProviders/Simulation/VirtualBundledAssetProvider.cs b/Runtime/ResourceManager/ResourceProviders/Simulation/VirtualBundledAssetProvider.cs index 9bff4f3f..8755652c 100644 --- a/Runtime/ResourceManager/ResourceProviders/Simulation/VirtualBundledAssetProvider.cs +++ b/Runtime/ResourceManager/ResourceProviders/Simulation/VirtualBundledAssetProvider.cs @@ -21,26 +21,6 @@ public override long ComputeSize(IResourceLocation location, ResourceManager res var id = resourceManager == null ? location.InternalId : resourceManager.TransformInternalId(location); if (!ResourceManagerConfig.IsPathRemote(id)) return 0; - - var locHash = Hash128.Parse(Hash); - if (!locHash.isValid) - return BundleSize; - -#if ENABLE_CACHING - if (locHash.isValid) //If we have a hash, ensure that our desired version is cached. - { - if (Caching.IsVersionCached(BundleName, locHash)) - return 0; - return BundleSize; - } - else //If we don't have a hash, any cached version will do. - { - List versions = new List(); - Caching.GetCachedVersions(BundleName, versions); - if (versions.Count > 0) - return 0; - } -#endif //ENABLE_CACHING return BundleSize; } } diff --git a/Runtime/ResourceProviders/ContentCatalogProvider.cs b/Runtime/ResourceProviders/ContentCatalogProvider.cs index f71dd83c..01909e70 100644 --- a/Runtime/ResourceProviders/ContentCatalogProvider.cs +++ b/Runtime/ResourceProviders/ContentCatalogProvider.cs @@ -3,10 +3,12 @@ using System.ComponentModel; using System.IO; using UnityEngine.AddressableAssets.ResourceLocators; +using UnityEngine.Networking; using UnityEngine.ResourceManagement; using UnityEngine.ResourceManagement.AsyncOperations; using UnityEngine.ResourceManagement.ResourceLocations; using UnityEngine.ResourceManagement.ResourceProviders; +using UnityEngine.ResourceManagement.Util; namespace UnityEngine.AddressableAssets.ResourceProviders { @@ -94,9 +96,9 @@ public void Start(ProvideHandle providerInterface, bool disableCatalogUpdateOnSt Addressables.LogFormat("Addressables - Using content catalog from {0}.", idToLoad); - bool isLocalCatalog = idToLoad.Equals(GetTransformedInternalId(m_ProviderInterface.Location)); + bool loadCatalogFromLocalBundle = isLocalCatalogInBundle && CanLoadCatalogFromBundle(idToLoad, m_ProviderInterface.Location); - LoadCatalog(idToLoad, isLocalCatalogInBundle, isLocalCatalog); + LoadCatalog(idToLoad, loadCatalogFromLocalBundle); } bool WaitForCompletionCallback() @@ -130,13 +132,24 @@ public void Release() m_ContentCatalogData?.CleanData(); } - internal void LoadCatalog(string idToLoad,bool isLocalCatalogInBundle, bool isLocalCatalog) + internal bool CanLoadCatalogFromBundle(string idToLoad, IResourceLocation location) + { + return Path.GetExtension(idToLoad) == ".bundle" && + idToLoad.Equals(GetTransformedInternalId(location)); + } + + internal void LoadCatalog(string idToLoad, bool loadCatalogFromLocalBundle) { try { - if (isLocalCatalogInBundle && isLocalCatalog) + ProviderLoadRequestOptions providerLoadRequestOptions = null; + if (m_ProviderInterface.Location.Data is ProviderLoadRequestOptions providerData) + providerLoadRequestOptions = providerData.Copy(); + + if (loadCatalogFromLocalBundle) { - m_BundledCatalog = new BundledCatalog(idToLoad); + int webRequestTimeout = providerLoadRequestOptions?.WebRequestTimeout ?? 0; + m_BundledCatalog = new BundledCatalog(idToLoad, webRequestTimeout); m_BundledCatalog.OnLoaded += ccd => { m_ContentCatalogData = ccd; @@ -148,9 +161,8 @@ internal void LoadCatalog(string idToLoad,bool isLocalCatalogInBundle, bool isLo { ResourceLocationBase location = new ResourceLocationBase(idToLoad, idToLoad, typeof(JsonAssetProvider).FullName, typeof(ContentCatalogData)); - if (m_ProviderInterface.Location.Data is ProviderLoadRequestOptions providerData) - location.Data = providerData.Copy(); - + location.Data = providerLoadRequestOptions; + m_ContentCatalogDataLoadOp = m_ProviderInterface.ResourceManager.ProvideResource(location); m_ContentCatalogDataLoadOp.Completed += CatalogLoadOpCompleteCallback; } @@ -176,13 +188,16 @@ internal class BundledCatalog internal AssetBundle m_CatalogAssetBundle; private AssetBundleRequest m_LoadTextAssetRequest; private ContentCatalogData m_CatalogData; + private WebRequestQueueOperation m_WebRequestQueueOperation; + private AsyncOperation m_RequestOperation; + private int m_WebRequestTimeout; public event Action OnLoaded; public bool OpInProgress => m_OpInProgress; public bool OpIsSuccess => !m_OpInProgress && m_CatalogData != null; - public BundledCatalog(string bundlePath) + public BundledCatalog(string bundlePath, int webRequestTimeout = 0) { if (string.IsNullOrEmpty(bundlePath)) { @@ -194,6 +209,7 @@ public BundledCatalog(string bundlePath) } m_BundlePath = bundlePath; + m_WebRequestTimeout = webRequestTimeout; } ~BundledCatalog() @@ -217,24 +233,74 @@ public void LoadCatalogFromBundleAsync() } m_OpInProgress = true; - m_LoadBundleRequest = AssetBundle.LoadFromFileAsync(m_BundlePath); - m_LoadBundleRequest.completed += loadOp => + + if (ResourceManagerConfig.ShouldPathUseWebRequest(m_BundlePath)) { - if (loadOp is AssetBundleCreateRequest createRequest && createRequest.assetBundle != null) + var req = UnityWebRequestAssetBundle.GetAssetBundle(m_BundlePath); + if (m_WebRequestTimeout > 0) + req.timeout = m_WebRequestTimeout; + + m_WebRequestQueueOperation = WebRequestQueue.QueueRequest(req); + if (m_WebRequestQueueOperation.IsDone) { - m_CatalogAssetBundle = createRequest.assetBundle; - m_LoadTextAssetRequest = m_CatalogAssetBundle.LoadAllAssetsAsync(); - if (m_LoadTextAssetRequest.isDone) - LoadTextAssetRequestComplete(m_LoadTextAssetRequest); - m_LoadTextAssetRequest.completed += LoadTextAssetRequestComplete; - } + m_RequestOperation = m_WebRequestQueueOperation.Result; + if (m_RequestOperation.isDone) + WebRequestOperationCompleted(m_RequestOperation); + else + m_RequestOperation.completed += WebRequestOperationCompleted; + } else { - Addressables.LogError($"Unable to load dependent bundle from location : {m_BundlePath}"); - m_OpInProgress = false; + m_WebRequestQueueOperation.OnComplete += asyncOp => + { + m_RequestOperation = asyncOp; + m_RequestOperation.completed += WebRequestOperationCompleted; + }; } - }; + } + else + { + m_LoadBundleRequest = AssetBundle.LoadFromFileAsync(m_BundlePath); + m_LoadBundleRequest.completed += loadOp => + { + if (loadOp is AssetBundleCreateRequest createRequest && createRequest.assetBundle != null) + { + m_CatalogAssetBundle = createRequest.assetBundle; + m_LoadTextAssetRequest = m_CatalogAssetBundle.LoadAllAssetsAsync(); + if (m_LoadTextAssetRequest.isDone) + LoadTextAssetRequestComplete(m_LoadTextAssetRequest); + m_LoadTextAssetRequest.completed += LoadTextAssetRequestComplete; + } + else + { + Addressables.LogError($"Unable to load dependent bundle from location : {m_BundlePath}"); + m_OpInProgress = false; + } + }; + } } + + private void WebRequestOperationCompleted(AsyncOperation op) + { + UnityWebRequestAsyncOperation remoteReq = op as UnityWebRequestAsyncOperation; + var webReq = remoteReq.webRequest; + DownloadHandlerAssetBundle downloadHandler = webReq.downloadHandler as DownloadHandlerAssetBundle; + if (!UnityWebRequestUtilities.RequestHasErrors(webReq, out UnityWebRequestResult uwrResult)) + { + m_CatalogAssetBundle = downloadHandler.assetBundle; + m_LoadTextAssetRequest = m_CatalogAssetBundle.LoadAllAssetsAsync(); + if (m_LoadTextAssetRequest.isDone) + LoadTextAssetRequestComplete(m_LoadTextAssetRequest); + m_LoadTextAssetRequest.completed += LoadTextAssetRequestComplete; + } + else + { + Addressables.LogError($"Unable to load dependent bundle from location : {m_BundlePath}"); + m_OpInProgress = false; + } + webReq.Dispose(); + } + void LoadTextAssetRequestComplete(AsyncOperation op) { if (op is AssetBundleRequest loadRequest diff --git a/Tests/Editor/AddressableAssetEntryTreeViewTests.cs b/Tests/Editor/AddressableAssetEntryTreeViewTests.cs index 920cfe24..db88fa17 100644 --- a/Tests/Editor/AddressableAssetEntryTreeViewTests.cs +++ b/Tests/Editor/AddressableAssetEntryTreeViewTests.cs @@ -6,6 +6,7 @@ using UnityEditor.AddressableAssets.Settings; using UnityEditor.AddressableAssets.Settings.GroupSchemas; using UnityEditor.IMGUI.Controls; +using UnityEngine; namespace UnityEditor.AddressableAssets.Tests { @@ -277,6 +278,75 @@ public void Search_Hierarchical_WithGroupHierarchyEnabled_WhenAssetFilePathMatch // Last child is the full name of the group Assert.NotNull(result.FirstOrDefault(c => c.displayName == nameWithDashes)); } + + [Test] + public void CopyAddressesToClipboard_Simple() + { + List nodesToSelect = new List(); + + AddressableAssetEntry entry1 = new AddressableAssetEntry("0001", "address1", null, false); + + nodesToSelect.Add(new AssetEntryTreeViewItem(entry1, 0)); + + //Save users previous clipboard so it doesn't get eaten during test + string previousClipboard = GUIUtility.systemCopyBuffer; + + AddressableAssetEntryTreeView.CopyAddressesToClipboard(nodesToSelect); + + string result = GUIUtility.systemCopyBuffer; + GUIUtility.systemCopyBuffer = previousClipboard; + + Assert.AreEqual("address1", result, "Entry's address was incorrectly copied."); + } + + [Test] + public void CopyAddressesToClipboard_Multiple() + { + List nodesToSelect = new List(); + + AddressableAssetEntry entry1 = new AddressableAssetEntry("0001", "address1", null, false); + AddressableAssetEntry entry2 = new AddressableAssetEntry("0002", "address2", null, false); + AddressableAssetEntry entry3 = new AddressableAssetEntry("0003", "address3", null, false); + + nodesToSelect.Add(new AssetEntryTreeViewItem(entry1, 0)); + nodesToSelect.Add(new AssetEntryTreeViewItem(entry2, 0)); + nodesToSelect.Add(new AssetEntryTreeViewItem(entry3, 0)); + + //Save users previous clipboard so it doesn't get eaten during test + string previousClipboard = GUIUtility.systemCopyBuffer; + + AddressableAssetEntryTreeView.CopyAddressesToClipboard(nodesToSelect); + + string result = GUIUtility.systemCopyBuffer; + GUIUtility.systemCopyBuffer = previousClipboard; + + Assert.AreEqual("address1,address2,address3", result, "Entry's address was incorrectly copied."); + } + + [Test] + public void CopyAddressesToClipboard_MaintainsOrder() + { + List nodesToSelect = new List(); + + AddressableAssetEntry entry1 = new AddressableAssetEntry("0001", "address1", null, false); + AddressableAssetEntry entry2 = new AddressableAssetEntry("0002", "address2", null, false); + AddressableAssetEntry entry3 = new AddressableAssetEntry("0003", "address3", null, false); + + nodesToSelect.Add(new AssetEntryTreeViewItem(entry2, 0)); + nodesToSelect.Add(new AssetEntryTreeViewItem(entry3, 0)); + nodesToSelect.Add(new AssetEntryTreeViewItem(entry1, 0)); + + //Save users previous clipboard so it doesn't get eaten during test + string previousClipboard = GUIUtility.systemCopyBuffer; + + AddressableAssetEntryTreeView.CopyAddressesToClipboard(nodesToSelect); + + string result = GUIUtility.systemCopyBuffer; + GUIUtility.systemCopyBuffer = previousClipboard; + + Assert.AreEqual("address2,address3,address1", result, "Entry's address was incorrectly copied."); + } + List GetAllEntries(bool includeSubObjects = false) { diff --git a/Tests/Editor/AddressableAssetSettingsTests.cs b/Tests/Editor/AddressableAssetSettingsTests.cs index 3c703e23..0c704e20 100644 --- a/Tests/Editor/AddressableAssetSettingsTests.cs +++ b/Tests/Editor/AddressableAssetSettingsTests.cs @@ -239,6 +239,76 @@ public void CreateUpdateNewEntry() Assert.IsNull(Settings.FindAssetEntry(entry.guid)); } + [Test] + public void CreateOrMoveEntries_CreatesNewEntries() + { + string guid1 = "guid1"; + string guid2 = "guid2"; + string guid3 = "guid3"; + + Settings.CreateOrMoveEntries(new List() { guid1, guid2, guid3 }, Settings.DefaultGroup, + new List(), + new List()); + + Assert.IsNotNull(Settings.FindAssetEntry(guid1)); + Assert.IsNotNull(Settings.FindAssetEntry(guid2)); + Assert.IsNotNull(Settings.FindAssetEntry(guid3)); + + Settings.RemoveAssetEntry(guid1); + Settings.RemoveAssetEntry(guid2); + Settings.RemoveAssetEntry(guid3); + } + + [Test] + public void CreateOrMoveEntries_MovesEntriesThatAlreadyExist() + { + string guid1 = "guid1"; + string guid2 = "guid2"; + string guid3 = "guid3"; + var group = Settings.CreateGroup("SeparateGroup", false, false, true, new List()); + group.AddAssetEntry(Settings.CreateEntry(guid1, "addr1", group,false)); + group.AddAssetEntry(Settings.CreateEntry(guid2, "addr2", group,false)); + group.AddAssetEntry(Settings.CreateEntry(guid3, "addr3", group,false)); + + Settings.CreateOrMoveEntries(new List() { guid1, guid2, guid3 }, Settings.DefaultGroup, + new List(), + new List()); + + Assert.IsNull(group.GetAssetEntry(guid1)); + Assert.IsNull(group.GetAssetEntry(guid2)); + Assert.IsNull(group.GetAssetEntry(guid3)); + + Assert.IsNotNull(Settings.DefaultGroup.GetAssetEntry(guid1)); + Assert.IsNotNull(Settings.DefaultGroup.GetAssetEntry(guid2)); + Assert.IsNotNull(Settings.DefaultGroup.GetAssetEntry(guid3)); + + Settings.RemoveGroup(group); + } + + [Test] + public void CreateOrMoveEntries_Creates_AndMovesExistingEntries_InMixedLists() + { + string guid1 = "guid1"; + string guid2 = "guid2"; + string guid3 = "guid3"; + var group = Settings.CreateGroup("SeparateGroup", false, false, true, new List()); + group.AddAssetEntry(Settings.CreateEntry(guid1, "addr1", group,false)); + group.AddAssetEntry(Settings.CreateEntry(guid3, "addr3", group,false)); + + Settings.CreateOrMoveEntries(new List() { guid1, guid2, guid3 }, Settings.DefaultGroup, + new List(), + new List()); + + Assert.IsNull(group.GetAssetEntry(guid1)); + Assert.IsNull(group.GetAssetEntry(guid3)); + + Assert.IsNotNull(Settings.DefaultGroup.GetAssetEntry(guid1)); + Assert.IsNotNull(Settings.DefaultGroup.GetAssetEntry(guid2)); + Assert.IsNotNull(Settings.DefaultGroup.GetAssetEntry(guid3)); + + Settings.RemoveGroup(group); + } + [Test] public void CannotCreateOrMoveWithoutGuid() { diff --git a/Tests/Editor/AddressableAssetsWindowTests.cs b/Tests/Editor/AddressableAssetsWindowTests.cs index e0d4927c..5f6c5619 100644 --- a/Tests/Editor/AddressableAssetsWindowTests.cs +++ b/Tests/Editor/AddressableAssetsWindowTests.cs @@ -1,5 +1,6 @@ using NUnit.Framework; using System.Collections.Generic; +using System.IO; using UnityEditor.AddressableAssets.GUI; using UnityEditor.AddressableAssets.Settings; using UnityEngine; @@ -17,7 +18,67 @@ public void AddressableAssetWindow_OfferToConvert_CantConvertWithNoBundles() Assert.AreEqual(prevGroupCount, Settings.groups.Count); Object.DestroyImmediate(aaWindow); } - + + [Test] + public void AddressableAssetWindow_SimplifyAddress_ReturnsFileNameOnly() + { + string assetPath = AssetDatabase.GUIDToAssetPath(m_AssetGUID); + var entry = Settings.CreateOrMoveEntry(m_AssetGUID, Settings.DefaultGroup); + Assert.AreEqual(assetPath, entry.address); + + AddressableAssetEntryTreeView treeView = new AddressableAssetEntryTreeView(Settings); + treeView.SimplifyAddressesImpl(new List() { new AssetEntryTreeViewItem(entry, 1) }); + + Assert.AreEqual(Path.GetFileNameWithoutExtension(assetPath), entry.address); + } + + [Test] + public void AddressableAssetWindow_RemovedEntries_AreNoLongerPresent() + { + var entry = Settings.CreateOrMoveEntry(m_AssetGUID, Settings.DefaultGroup); + + AddressableAssetEntryTreeView treeView = new AddressableAssetEntryTreeView(Settings); + treeView.RemoveEntryImpl(new List() { new AssetEntryTreeViewItem(entry, 1) }, true); + + Assert.IsNull(Settings.FindAssetEntry(m_AssetGUID)); + } + + [Test] + public void AddressableAssetWindow_RemoveGroup_GroupGetsRemovedCorrectly() + { + var group = Settings.CreateGroup("RemoveMeGroup", false, false, true, new List()); + AddressableAssetEntryTreeView treeView = new AddressableAssetEntryTreeView(Settings); + treeView.RemoveGroupImpl(new List() { new AssetEntryTreeViewItem(group, 1) }, true); + Assert.IsNull(Settings.FindGroup("RemoveMeGroup")); + } + + [Test] + public void AddressableAssetWindow_RemoveMissingReferences_RemovesAllNullReferences() + { + Settings.groups.Add(null); + Settings.groups.Add(null); + + AddressableAssetEntryTreeView treeView = new AddressableAssetEntryTreeView(Settings); + treeView.RemoveMissingReferencesImpl(); + foreach(var group in Settings.groups) + Assert.IsNotNull(group); + } + + [Test] + public void AddressableAssetWindow_SetDefaultGroup_SetsTheSpecifiedGroupToDefault() + { + var savedDefaultGroup = Settings.DefaultGroup; + var newDefaultGroup = Settings.CreateGroup("NewDefaultGroup", false, false, true, new List()); + AddressableAssetEntryTreeView treeView = new AddressableAssetEntryTreeView(Settings); + + treeView.SetGroupAsDefault(new List() { new AssetEntryTreeViewItem(newDefaultGroup, 1) }); + + Assert.AreEqual(newDefaultGroup, Settings.DefaultGroup); + + Settings.DefaultGroup = savedDefaultGroup; + Settings.RemoveGroup(newDefaultGroup); + } + [Test] public void AddressableAssetWindow_CanSelectGroupTreeViewByAddressableAssetEntries() { diff --git a/Tests/Editor/Build/BuildScriptTests.cs b/Tests/Editor/Build/BuildScriptTests.cs index 50f87f4d..20d1bbaa 100644 --- a/Tests/Editor/Build/BuildScriptTests.cs +++ b/Tests/Editor/Build/BuildScriptTests.cs @@ -453,7 +453,6 @@ public void WhenAddressHasSquareBrackets_AndContentCatalogsAreCreated_BuildFails AddressableAssetEntry entry = Settings.CreateOrMoveEntry(m_AssetGUID, Settings.DefaultGroup); entry.address = "[test]"; LogAssert.Expect(LogType.Error, $"Address '{entry.address}' cannot contain '[ ]'."); - foreach (IDataBuilder db in Settings.DataBuilders) { if (db.GetType() == typeof(BuildScriptFastMode) || db.GetType() == typeof(BuildScriptPackedPlayMode)) @@ -463,7 +462,7 @@ public void WhenAddressHasSquareBrackets_AndContentCatalogsAreCreated_BuildFails db.BuildData(context); else if (db.CanBuildData()) db.BuildData(context); - LogAssert.Expect(LogType.Error, new Regex(@"Address \'\[test\]\' cannot contain \'\[ \]\'")); + LogAssert.Expect(LogType.Error, "Address '[test]' cannot contain '[ ]'."); } Settings.RemoveAssetEntry(m_AssetGUID, false); @@ -484,16 +483,16 @@ public void WhenFileTypeIsInvalid_AndContentCatalogsAreCreated_BuildFails() foreach (IDataBuilder db in Settings.DataBuilders) { - if (db.GetType() == typeof(BuildScriptFastMode) || db.GetType() == typeof(BuildScriptPackedPlayMode)) + if (db.GetType() == typeof(BuildScriptFastMode) || + db.GetType() == typeof(BuildScriptPackedPlayMode)) continue; if (db.CanBuildData()) db.BuildData(context); else if (db.CanBuildData()) db.BuildData(context); - LogAssert.Expect(LogType.Error, new Regex($".*{path}.*import failed.*")); + LogAssert.Expect(LogType.Error, "Cannot recognize file type for entry located at 'Assets/UnityEditor.AddressableAssets.Tests.BuildScriptTests_Tests/fake.file'. Asset import failed for using an unsupported file type."); } - Settings.RemoveAssetEntry(guid, false); AssetDatabase.DeleteAsset(path); } diff --git a/Tests/Editor/Build/GenerateLocationListsTaskTests.cs b/Tests/Editor/Build/GenerateLocationListsTaskTests.cs index b0b909c4..28b63ea5 100644 --- a/Tests/Editor/Build/GenerateLocationListsTaskTests.cs +++ b/Tests/Editor/Build/GenerateLocationListsTaskTests.cs @@ -208,12 +208,76 @@ public void WhenBuildTargetIsWindowsOrXBox_BackSlashUsedInLoadPath(string id, Bu { AddressableAssetGroup group = m_Settings.CreateGroup($"xyz", false, false, false, null, typeof(BundledAssetGroupSchema)); var bag = group.GetSchema(); - var expectedPath = $"{bag.LoadPath.GetValue(m_Settings)}{expected}"; + var expectedPath = $"{bag.LoadPath.GetValue(m_Settings)}{expected}".Replace('/', GenerateLocationListsTask.PathSeparatorForPlatform(target)); var path = GenerateLocationListsTask.GetLoadPath(group, id, target); Assert.AreEqual(expectedPath, path); m_Settings.RemoveGroup(group); } + [Test] + [TestCase("abc", BuildTarget.XboxOne)] + [TestCase("abc", BuildTarget.StandaloneWindows64)] + [TestCase("abc", BuildTarget.iOS)] + [TestCase("abc", BuildTarget.Android)] + [TestCase("abc", BuildTarget.StandaloneLinux64)] + [TestCase("abc", BuildTarget.Switch)] + [TestCase("abc", BuildTarget.StandaloneOSX)] + public void WhenPathIsRemote_WithTrailingSlash_PathIsNotMalformed(string id, BuildTarget target) + { + //Setup + string baseId = m_Settings.profileSettings.Reset(); + string profileId = m_Settings.profileSettings.AddProfile("remote", baseId); + m_Settings.profileSettings.SetValue(profileId, "RemoteLoadPath", "http://127.0.0.1:80/"); + m_Settings.activeProfileId = profileId; + AddressableAssetGroup group = m_Settings.CreateGroup($"xyz", false, false, false, null, typeof(BundledAssetGroupSchema)); + var bag = group.GetSchema(); + bag.LoadPath.Id = m_Settings.profileSettings.GetVariableId("RemoteLoadPath"); + + //Test + var path = GenerateLocationListsTask.GetLoadPath(group, id, target); + string pahWithoutHttp = path.Replace("http://", ""); + + //Assert + Assert.IsFalse(path.Contains("/\\")); + Assert.IsFalse(pahWithoutHttp.Contains("//")); + + //Cleanup + m_Settings.RemoveGroup(group); + m_Settings.activeProfileId = baseId; + } + + [Test] + [TestCase("abc", BuildTarget.XboxOne)] + [TestCase("abc", BuildTarget.StandaloneWindows64)] + [TestCase("abc", BuildTarget.iOS)] + [TestCase("abc", BuildTarget.Android)] + [TestCase("abc", BuildTarget.StandaloneLinux64)] + [TestCase("abc", BuildTarget.Switch)] + [TestCase("abc", BuildTarget.StandaloneOSX)] + public void WhenPathIsRemote_WithoutTrailingSlash_PathIsNotMalformed(string id, BuildTarget target) + { + //Setup + string baseId = m_Settings.profileSettings.Reset(); + string profileId = m_Settings.profileSettings.AddProfile("remote", baseId); + m_Settings.profileSettings.SetValue(profileId, "RemoteLoadPath", "http://127.0.0.1:80"); + m_Settings.activeProfileId = profileId; + AddressableAssetGroup group = m_Settings.CreateGroup($"xyz", false, false, false, null, typeof(BundledAssetGroupSchema)); + var bag = group.GetSchema(); + bag.LoadPath.Id = m_Settings.profileSettings.GetVariableId("RemoteLoadPath"); + + //Test + var path = GenerateLocationListsTask.GetLoadPath(group, id, target); + string pahWithoutHttp = path.Replace("http://", ""); + + //Assert + Assert.IsFalse(path.Contains("/\\")); + Assert.IsFalse(pahWithoutHttp.Contains("//")); + + //Cleanup + m_Settings.RemoveGroup(group); + m_Settings.activeProfileId = baseId; + } + //[Test] //public void WhenEntryAddressContainsBrackets_ExceptionIsThrown() //{ diff --git a/Tests/Editor/ContentCatalogTests.cs b/Tests/Editor/ContentCatalogTests.cs index 4de1490e..085f55f0 100644 --- a/Tests/Editor/ContentCatalogTests.cs +++ b/Tests/Editor/ContentCatalogTests.cs @@ -108,7 +108,8 @@ public void AssetBundleRequestOptionsTest() Hash = new Hash128(1, 2, 3, 4).ToString(), RedirectLimit = 4, RetryCount = 7, - Timeout = 12 + Timeout = 12, + AssetLoadMode = AssetLoadMode.AllPackedAssetsAndDependencies }; var dataEntry = new ContentCatalogDataEntry(typeof(ContentCatalogData), "internalId", "provider", new object[] { 1 }, null, options); var entries = new List(); @@ -128,6 +129,7 @@ public void AssetBundleRequestOptionsTest() Assert.AreEqual(locOptions.RedirectLimit, options.RedirectLimit); Assert.AreEqual(locOptions.RetryCount, options.RetryCount); Assert.AreEqual(locOptions.Timeout, options.Timeout); + Assert.AreEqual(locOptions.AssetLoadMode, options.AssetLoadMode); } [Test] diff --git a/Tests/Runtime/AddressablesIntegrationTests.cs b/Tests/Runtime/AddressablesIntegrationTests.cs index 67634fb4..95818994 100644 --- a/Tests/Runtime/AddressablesIntegrationTests.cs +++ b/Tests/Runtime/AddressablesIntegrationTests.cs @@ -218,8 +218,25 @@ public override void DeleteTempFiles() AddressablesTestUtility.TearDown("BuildScriptPackedMode", PathFormat, "BASE"); AddressablesTestUtility.TearDown(TypeName, PathFormat, "BASE"); } - } + [UnityTest] + public IEnumerator GetDownloadSize_CalculatesCachedBundles() + { + return GetDownloadSize_CalculatesCachedBundlesInternal(); + } + + [UnityTest] + public IEnumerator GetDownloadSize_WithList_CalculatesCachedBundles() + { + return GetDownloadSize_WithList_CalculatesCachedBundlesInternal(); + } + + [UnityTest] + public IEnumerator GetDownloadSize_WithList_CalculatesCorrectSize_WhenAssetsReferenceSameBundle() + { + return GetDownloadSize_WithList_CalculatesCorrectSize_WhenAssetsReferenceSameBundleInternal(); + } + } #endif class AddressablesIntegrationPlayer : AddressablesIntegrationTests @@ -238,5 +255,23 @@ protected override ILocationSizeData CreateLocationSizeData(string name, long si Hash = hash }; } + + [UnityTest] + public IEnumerator GetDownloadSize_CalculatesCachedBundles() + { + return GetDownloadSize_CalculatesCachedBundlesInternal(); + } + + [UnityTest] + public IEnumerator GetDownloadSize_WithList_CalculatesCachedBundles() + { + return GetDownloadSize_WithList_CalculatesCachedBundlesInternal(); + } + + [UnityTest] + public IEnumerator GetDownloadSize_WithList_CalculatesCorrectSize_WhenAssetsReferenceSameBundle() + { + return GetDownloadSize_WithList_CalculatesCorrectSize_WhenAssetsReferenceSameBundleInternal(); + } } } diff --git a/Tests/Runtime/AddressablesIntegrationTestsImpl.cs b/Tests/Runtime/AddressablesIntegrationTestsImpl.cs index 467c6f6d..210ac6e3 100644 --- a/Tests/Runtime/AddressablesIntegrationTestsImpl.cs +++ b/Tests/Runtime/AddressablesIntegrationTestsImpl.cs @@ -1127,8 +1127,7 @@ public IEnumerator VerifyDownloadSize() dOp.Release(); } - [UnityTest] - public IEnumerator GetDownloadSize_CalculatesCachedBundles() + public IEnumerator GetDownloadSize_CalculatesCachedBundlesInternal() { #if ENABLE_CACHING yield return Init(); @@ -1179,8 +1178,7 @@ public IEnumerator GetDownloadSize_CalculatesCachedBundles() #endif } - [UnityTest] - public IEnumerator GetDownloadSize_WithList_CalculatesCachedBundles() + public IEnumerator GetDownloadSize_WithList_CalculatesCachedBundlesInternal() { #if ENABLE_CACHING yield return Init(); @@ -1237,8 +1235,7 @@ public IEnumerator GetDownloadSize_WithList_CalculatesCachedBundles() #endif } - [UnityTest] - public IEnumerator GetDownloadSize_WithList_CalculatesCorrectSize_WhenAssetsReferenceSameBundle() + public IEnumerator GetDownloadSize_WithList_CalculatesCorrectSize_WhenAssetsReferenceSameBundleInternal() { #if ENABLE_CACHING yield return Init(); @@ -1553,6 +1550,38 @@ public IEnumerator LoadAsset_WhenEntryExists_ReturnsAsset() op.Release(); } + [UnityTest] + public IEnumerator LoadAsset_SuccessfulWhenLoadAssetMode_LoadAllAssets() + { + yield return Init(); + if (string.IsNullOrEmpty(TypeName) || TypeName == "BuildScriptFastMode" || TypeName == "BuildScriptVirtualMode") + { + Assert.Ignore($"Skipping test {nameof(LoadAsset_SuccessfulWhenLoadAssetMode_LoadAllAssets)} for {TypeName}, AssetBundle based test."); + } + + string label = AddressablesTestUtility.GetPrefabUniqueLabel("BASE", 0); + + var locationHandle = m_Addressables.LoadResourceLocationsAsync(label); + yield return locationHandle; + Assert.IsTrue(locationHandle.Result != null, "Failed to get Location for " + label); + Assert.AreEqual(1, locationHandle.Result.Count, "Failed to get Location for " + label); + IResourceLocation loc = locationHandle.Result[0]; + Addressables.Release(locationHandle); + + foreach (IResourceLocation dependency in loc.Dependencies) + { + var locOptions = dependency.Data as AssetBundleRequestOptions; + Assert.IsNotNull(locOptions, "Location dependency did not contain expected AssetBundleRequestOptions data"); + locOptions.AssetLoadMode = AssetLoadMode.AllPackedAssetsAndDependencies; + } + + AsyncOperationHandle op = m_Addressables.LoadAssetAsync(loc); + yield return op; + Assert.AreEqual(AsyncOperationStatus.Succeeded, op.Status, "Loading of " + label + " failed."); + Assert.IsTrue(op.Result != null, "Loading of " + label + " was successful, but result was null."); + op.Release(); + } + [UnityTest] public IEnumerator LoadAssetWithWrongType_WhenEntryExists_Fails() { diff --git a/Tests/Runtime/InitializationObjectsAsyncTests.cs b/Tests/Runtime/InitializationObjectsAsyncTests.cs index dcf58be0..2690d2db 100644 --- a/Tests/Runtime/InitializationObjectsAsyncTests.cs +++ b/Tests/Runtime/InitializationObjectsAsyncTests.cs @@ -197,12 +197,12 @@ public IEnumerator CacheInitializationObject_FullySetsCachingData() { CacheDirectoryOverride = Caching.currentCacheForWriting.path, CompressionEnabled = Caching.compressionEnabled, - ExpirationDelay = Caching.currentCacheForWriting.expirationDelay, + //ExpirationDelay = Caching.currentCacheForWriting.expirationDelay, MaximumCacheSize = Caching.currentCacheForWriting.maximumAvailableStorageSpace }; string cacheDirectoryOverride = "TestDirectory"; - int expirationDelay = 4321; + //int expirationDelay = 4321; long maxCacheSize = 9876; bool compressionEnabled = !preTestCacheData.CompressionEnabled; @@ -210,7 +210,7 @@ public IEnumerator CacheInitializationObject_FullySetsCachingData() { CacheDirectoryOverride = cacheDirectoryOverride, CompressionEnabled = compressionEnabled, - ExpirationDelay = expirationDelay, + //ExpirationDelay = expirationDelay, LimitCacheSize = true, MaximumCacheSize = maxCacheSize }; @@ -222,7 +222,7 @@ public IEnumerator CacheInitializationObject_FullySetsCachingData() yield return handle; Assert.AreEqual(cacheDirectoryOverride, Caching.currentCacheForWriting.path); - Assert.AreEqual(expirationDelay, Caching.currentCacheForWriting.expirationDelay); + //Assert.AreEqual(expirationDelay, Caching.currentCacheForWriting.expirationDelay); Assert.AreEqual(compressionEnabled, Caching.compressionEnabled); Assert.AreEqual(maxCacheSize, Caching.currentCacheForWriting.maximumAvailableStorageSpace); @@ -230,7 +230,7 @@ public IEnumerator CacheInitializationObject_FullySetsCachingData() Cache cache = Caching.GetCacheByPath(preTestCacheData.CacheDirectoryOverride); Caching.compressionEnabled = preTestCacheData.CompressionEnabled; cache.maximumAvailableStorageSpace = preTestCacheData.MaximumCacheSize; - cache.expirationDelay = preTestCacheData.ExpirationDelay; + //cache.expirationDelay = preTestCacheData.ExpirationDelay; Caching.currentCacheForWriting = cache; handle.Release(); diff --git a/Tests/Runtime/ResourceProviders/ContentCatalogProviderTests.cs b/Tests/Runtime/ResourceProviders/ContentCatalogProviderTests.cs index 1366d112..8cfc25d2 100644 --- a/Tests/Runtime/ResourceProviders/ContentCatalogProviderTests.cs +++ b/Tests/Runtime/ResourceProviders/ContentCatalogProviderTests.cs @@ -13,6 +13,7 @@ using UnityEngine.ResourceManagement.ResourceLocations; using UnityEngine.ResourceManagement.ResourceProviders; using UnityEngine.TestTools; +using UnityEngine.AddressableAssets.ResourceLocators; namespace UnityEngine.AddressableAssets.ResourceProviders.Tests { @@ -208,7 +209,7 @@ public void BundledCatalog_LoadCatalogFromBundle_InvalidBundlePath_ShouldThrow(s { Assert.Throws(exceptionType, () => new ContentCatalogProvider.InternalOp.BundledCatalog(path)); } - + [UnityTest] [Ignore("https://jira.unity3d.com/browse/ADDR-1451")] public IEnumerator BundledCatalog_LoadCatalogFromBundle_InvalidBundleFileFormat_ShouldFail() @@ -218,13 +219,13 @@ public IEnumerator BundledCatalog_LoadCatalogFromBundle_InvalidBundleFileFormat_ var bytes = new byte[] { 1, 2, 3, 4, 5, 6 }; File.WriteAllBytes(bundleFilePath, bytes); - + #if UNITY_2019_4_OR_NEWER LogAssert.Expect(LogType.Error, new Regex("Failed to read data for the AssetBundle", RegexOptions.IgnoreCase)); #endif - + LogAssert.Expect(LogType.Error, new Regex("Unable to load dependent " + - $"bundle from location :", RegexOptions.IgnoreCase)); + $"bundle from location :", RegexOptions.IgnoreCase)); var bundledCatalog = new ContentCatalogProvider.InternalOp.BundledCatalog(bundleFilePath); bundledCatalog.LoadCatalogFromBundleAsync(); @@ -238,7 +239,7 @@ public IEnumerator BundledCatalog_LoadCatalogFromBundle_InvalidBundleFileFormat_ } [UnityTest] - public IEnumerator BundledCatalog_LoadCatalogFromBundle_ShouldLoadCatalogAndUnloadResources() + public IEnumerator BundledCatalog_WhenCatalogIsLocal_LoadCatalogFromBundle_ShouldLoadCatalogAndUnloadResources() { var bundleFilePath = Path.Combine(Addressables.RuntimePath, m_RuntimeCatalogFilename); @@ -256,6 +257,41 @@ public IEnumerator BundledCatalog_LoadCatalogFromBundle_ShouldLoadCatalogAndUnlo Assert.Null(bundledCatalog.m_CatalogAssetBundle); } + [UnityTest] + public IEnumerator BundledCatalog_WhenCatalogIsRemote_LoadCatalogFromBundle_ShouldLoadCatalogAndUnloadResources() + { + string localBundleFilePath = Path.Combine(Addressables.RuntimePath, m_RuntimeCatalogFilename); + string bundleFilePath = "file:///" + Path.GetFullPath(localBundleFilePath); + + var bundledCatalog = new ContentCatalogProvider.InternalOp.BundledCatalog(bundleFilePath); + bundledCatalog.LoadCatalogFromBundleAsync(); + bundledCatalog.OnLoaded += catalogData => + { + Assert.NotNull(catalogData); + Assert.AreEqual(ResourceManagerRuntimeData.kCatalogAddress, catalogData.ProviderId); + }; + + yield return new WaitWhile(() => bundledCatalog.OpInProgress); + + Assert.IsTrue(bundledCatalog.OpIsSuccess); + Assert.Null(bundledCatalog.m_CatalogAssetBundle); + } + + [UnityTest] + public IEnumerator BundledCatalog_WhenRemoteCatalogDoesNotExist_LoadCatalogFromBundle_LogsErrorAndOpFails() + { + string bundleFilePath = "file:///doesnotexist.bundle"; + + var bundledCatalog = new ContentCatalogProvider.InternalOp.BundledCatalog(bundleFilePath); + bundledCatalog.LoadCatalogFromBundleAsync(); + + LogAssert.Expect(LogType.Error, $"Unable to load dependent bundle from location : {bundleFilePath}"); + + yield return new WaitWhile(() => bundledCatalog.OpInProgress); + + Assert.IsFalse(bundledCatalog.OpIsSuccess); + } + [UnityTest] public IEnumerator BundledCatalog_LoadCatalogFromBundle_WhenCalledMultipleTimes_OpNotCompleted_FirstShouldSucceedAndOthersShouldFail() { @@ -302,11 +338,37 @@ public IEnumerator BundledCatalog_LoadCatalogFromBundle_WhenCalledMultipleTimes_ Assert.AreEqual(2, timesCalled); Assert.IsTrue(bundledCatalog.OpIsSuccess); } - + [Test] public void ContentCatalogProvider_InternalOp_LoadCatalog_InvalidId_Throws() { - Assert.Throws(() => new ContentCatalogProvider.InternalOp().LoadCatalog("fakeId", false,false)); + Assert.Throws(() => new ContentCatalogProvider.InternalOp().LoadCatalog("fakeId", false)); + } + + [TestCase("http://127.0.0.1/catalog.json", false)] + [TestCase("http://127.0.0.1/catalog.bundle", true)] + public void BundledCatalog_WhenRequestingRemoteCatalog_CanLoadCatalogFromBundle_ReturnsExpectedResult(string internalId, bool result) + { + var loc = new ResourceLocationBase(internalId, internalId, typeof(ContentCatalogProvider).FullName, typeof(IResourceLocator)); + ProviderOperation op = new ProviderOperation(); + op.Init(m_Addressables.ResourceManager, null, loc, new AsyncOperationHandle>()); + ProvideHandle handle = new ProvideHandle(m_Addressables.ResourceManager, op); + + bool loadCatalogFromLocalBundle = new ContentCatalogProvider.InternalOp().CanLoadCatalogFromBundle(internalId, handle.Location); + Assert.AreEqual(result, loadCatalogFromLocalBundle); + } + + [Test] + public void BundledCatalog_WhenRequestingLocalCatalog_CanLoadCatalogFromBundle_ReturnsTrue() + { + string internalId = Path.Combine(Addressables.RuntimePath, m_RuntimeCatalogFilename); + var loc = new ResourceLocationBase(internalId, internalId, typeof(ContentCatalogProvider).FullName, typeof(IResourceLocator)); + ProviderOperation op = new ProviderOperation(); + op.Init(m_Addressables.ResourceManager, null, loc, new AsyncOperationHandle>()); + ProvideHandle handle = new ProvideHandle(m_Addressables.ResourceManager, op); + + bool loadCatalogFromLocalBundle = new ContentCatalogProvider.InternalOp().CanLoadCatalogFromBundle(internalId, handle.Location); + Assert.IsTrue(loadCatalogFromLocalBundle); } } } diff --git a/ValidationExceptions.json b/ValidationExceptions.json index db3ab994..7f3c4d40 100644 --- a/ValidationExceptions.json +++ b/ValidationExceptions.json @@ -4,7 +4,7 @@ { "ValidationTest": "API Validation", "ExceptionError": "", - "PackageVersion": "1.18.9" + "PackageVersion": "1.18.11" } ] } diff --git a/package.json b/package.json index 79bd33e0..ebbe06a6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "com.unity.addressables", "displayName": "Addressables", - "version": "1.18.9", + "version": "1.18.11", "unity": "2018.4", "description": "The Addressable Asset System allows the developer to ask for an asset via its address. Once an asset (e.g. a prefab) is marked \"addressable\", it generates an address which can be called from anywhere. Wherever the asset resides (local or remote), the system will locate it and its dependencies, then return it.\n\nUse 'Window->Asset Management->Addressables' to begin working with the system.\n\nAddressables use asynchronous loading to support loading from any location with any collection of dependencies. Whether you have been using direct references, traditional asset bundles, or Resource folders, addressables provide a simpler way to make your game more dynamic. Addressables simultaneously opens up the world of asset bundles while managing all the complexity.\n\nFor usage samples, see github.com/Unity-Technologies/Addressables-Sample", "keywords": [ @@ -12,7 +12,7 @@ "assetbundles" ], "dependencies": { - "com.unity.scriptablebuildpipeline": "1.19.0", + "com.unity.scriptablebuildpipeline": "1.19.1", "com.unity.modules.assetbundle": "1.0.0", "com.unity.modules.imageconversion": "1.0.0", "com.unity.modules.jsonserialize": "1.0.0", @@ -22,10 +22,10 @@ "repository": { "url": "https://github.cds.internal.unity3d.com/unity/Addressables.git", "type": "git", - "revision": "0fc62dc1024e533883dfa7a8485a6a1e6ae99e0a" + "revision": "89efc9676f93524873726099d5b2dcb803322bb7" }, "upmCi": { - "footprint": "baab8e83bf1f1b32dd3cd9db6da06f4982e19cda" + "footprint": "63f44a12ac7b682878708297d046f1f9a3f754f1" }, "samples": [ {