diff --git a/CHANGELOG.md b/CHANGELOG.md index c2fc3418..e4c59e98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,20 @@ 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.16.8] - 2020-11-4 +- Added internal naming option for the Bundled Asset Group Schema. Instead of using the full path, there are options to use the asset guid or the hashcode of the guid. These values are stable and wont change if the asset path changes, reducing the need to rebuild a bundle if paths change but contents do not. The internal ids stored in the content catalog will generally be shorter than asset paths - 32 bytes for the full guid, 8 bytes for the guid hash. +- Added option to exclude sub catalog entries by file extension +- Added options to exclude catalog entries for address, labels, and guids +- Added option to optimize catalog size by extracting duplicated string in urls and file paths +- Fixed issue where ResourceLocations were returning null for the ResourceType. +- Added warning to build when an Addressable Group doesn't have any AddressableAssetGroupSchemas +- Fixed issue where resource folder search was case sensitive for Mac and Linux +- Fixed issue where warnings were getting logged incorrectly when marking an asset as Addressable using the checkbox in the inspector. +- Add Yamato trigger for release testing & rework variables into metafile +- Fixed issue where an AssetReference's cached asset is not reset when the underlying asset re-imports. +- Fixed issue where we were still checking for CRC when a bundle was cached. +- Fixed bug when using Play Mode Script "Use AssetDatabase (fastest)", and calling Addressables.LoadContentCatalogAsync would fail when it had not been cached. + ## [1.16.7] - 2020-10-21 - Fixed issue where InvalidHandle errors were getting thrown if an operation failed with releaseDependenciesOnFailure turned on. - Fixed group build and load paths not being saved when editing multiple groups at once diff --git a/Documentation~/AddressableAssetsAsyncOperationHandle.md b/Documentation~/AddressableAssetsAsyncOperationHandle.md index 51cf4530..5e1be53f 100644 --- a/Documentation~/AddressableAssetsAsyncOperationHandle.md +++ b/Documentation~/AddressableAssetsAsyncOperationHandle.md @@ -48,7 +48,11 @@ void Start() { ``` public IEnumerator Start() { AsyncOperationHandle handle = Addressables.LoadAssetAsync("mytexture"); - yield return handle; + + //if the handle is done, the yield return will still wait a frame, but we can skip that with an IsDone check + if(!handle.IsDone) + yield return handle; + if (handle.Status == AsyncOperationStatus.Succeeded) { Texture2D texture = handle.Result; // The texture is ready for use. diff --git a/Documentation~/AddressablesAPI.md b/Documentation~/AddressablesAPI.md index 4423063a..daab4602 100644 --- a/Documentation~/AddressablesAPI.md +++ b/Documentation~/AddressablesAPI.md @@ -5,14 +5,15 @@ uid: addressables-api This page exists in addition to the standard Addressable Assets [Scripting API documentation](xref:addressables-script-ref). The purpose of this expanded API documentation is to provide quick access and a deeper understanding of often-used or important API items, including code samples where relevant. -* [LoadingAddressableAssets](LoadingAddressableAssets.md) +* [BuildPlayerContent](BuildPlayerContent.md) +* [DownloadDependenciesAsync](DownloadDependenciesAsync.md) +* [ExceptionHandler](ExceptionHandler.md) * [InitializeAsync](InitializeAsync.md) -* [TransformInternalId](TransformInternalId.md) * [InstantiateAsync](InstantiateAsync.md) -* [DownloadDependenciesAsync](DownloadDependenciesAsync.md) * [LoadContentCatalogAsync](LoadContentCatalogAsync.md) -* [UpdateCatalogs](UpdateCatalogs.md) +* [LoadingAddressableAssets](LoadingAddressableAssets.md) +* [LoadResourceLocationsAsync](LoadResourceLocations.md) * [LoadSceneAsync](LoadSceneAsync.md) -* [ExceptionHandler](ExceptionHandler.md) -* [BuildPlayerContent](BuildPlayerContent.md) -* [LoadResourceLocationsAsync](LoadResourceLocations.md) \ No newline at end of file +* [TransformInternalId](TransformInternalId.md) +* [UpdateCatalogs](UpdateCatalogs.md) + diff --git a/Documentation~/AddressablesFAQ.md b/Documentation~/AddressablesFAQ.md new file mode 100644 index 00000000..b0ae95d9 --- /dev/null +++ b/Documentation~/AddressablesFAQ.md @@ -0,0 +1,50 @@ +--- +uid: addressables-faq +--- + +# Addressables FAQ + +### Is it better to have many small bundles or a few bigger ones? +There are a few key factors that go into deciding how many bundles to generate. +First, it's important to note that you control how many bundles you have both by how large your groups are, and by the groups' build settings. "Pack Together" for example, creates one bundle per group, while "Pack Separately" creates many. See [schema build settings for more information](xref:UnityEditor.AddressableAssets.Settings.GroupSchemas.BundledAssetGroupSchema.BundleMode). + +Once you know how to control bundle layout, the decision of how to set these up will be game specific. Here are key pieces of data to help make that decision: + +Dangers of too many bundles: +* Each bundle has memory overhead. Details are [on the memory management page](MemoryManagement.md#assetbundle-memory-overhead). This is tied to a number of factors, outlined on that page, but the short version is that this overhead can be significant. If you anticipate 100's or even 1000's of bundles loaded in memory at once, this could mean a noticeable amount of memory eaten up. +* There are concurrency limits for downloading bundles. If you have 1000's of bundles you need all at once, they cannot not all be downloaded at the same time. Some number will be downloaded, and as they finish, more will trigger. In practice this is a fairly minor concern, so minor that you'll often be gated by the total size of your download, rather than how many bundles it's broken into. +* Bundle information can bloat the catalog. To be able to download or load catalogs, we store string-based information about your bundles. 1000's of bundles worth of data can greatly increase the size of the catalog. + +Dangers of too few bundles: +* The UnityWebRequest (which we use to download) does not resume failed downloads. So if a large bundle downloading and your user loses connection, the download is started over once they regain connection. +* Items can be loaded individually from bundles, but cannot be unloaded individually. For example, if you have 10 materials in a bundle, load all 10, then tell Addressables to release 9 of them, all 10 will likely be in memory. This is also covered [on the memory management page](MemoryManagement.md#when-is-memory-cleared). + +### What compression settings are best? +Addressables provides three different options for bundle compression: Uncompressed, LZ4, and LZMA. Generally speaking, LZ4 should be used for local content, and LZMA for remote, but more details are outlined below as there can be exceptions to this. +You can set the compression option using the Advanced settings on each group. Compression does not affect in-memory size of your loaded content. +* Uncompressed - This option is largest on disk, and generally fasted to load. If your game happens to have space to spare, this option should at least be considered for local content. A key advantage of uncompressed bundles is how they handle being patched. If you are developing for a platform where the platform itself provides patching (such as Steam or Switch), uncompressed bundles provide the most accurate (smallest) patching. Either of the other compression options will cause at least some bloat of patches. +* LZ4 - If Uncompressed is not a viable option, then LZ4 should be used for all other local content. This is a chunk-based compression which provides the ability to load parts of the file without needing to load it in its entirety. +* LZMA - LZMA should be used for all remote content, but not for any local content. It provides the smallest bundle size, but is slow to load. If you were to store local bundles in LZMA you could create a smaller player, but load times would be significantly worse than uncompressed or LZ4. For downloaded bundles, we avoid the slow load time by recompressing the downloaded bundle when storing it in the asset bundle cache. By default, bundles will be stored in the cache Uncompressed. If you wish to compress the cache with LZ4, you can do so by creating a [`CacheInitializationSettings`](xref:UnityEditor.AddressableAssets.Settings.CacheInitializationSettings). See [Initialization Objects](AddressableAssetsDevelopmentCycle.md#initialization-objects) for more information about setting this up. + +Note that the hardware characteristics of a platform can mean that uncompressed bundles are not always the fastest to load. The maximum speed of loading uncompressed bundles is gated by IO speed, while the speed of loading LZ4-compressed bundles can be gated by either IO speed or CPU, depending on hardware. On most platforms, loading LZ4-compressed bundles is CPU bound, and loading uncompressed bundles will be faster. On platforms that have low IO speeds and high CPU speeds, LZ4 loading can be faster. It is always a good practice to run performance analysis to validate whether your game fits the common patterns, or needs some unique tweaking. + +More information on Unity's compression selection is available in the [Asset Bundle documentation](https://docs.unity3d.com/Manual/AssetBundles-Cache.html). + +### Are there ways to miminize the catalog size? +Currently there are two optimizations available. +1. Compress the local catalog. If your primary concern is how big the catalog is in your build, there is an option in the inspector for the top level settings of **Compress Local Catalog**. This option builds catalog that ships with your game into an asset bundle. Compressing the catalog makes the file itself smaller, but note that this does increase catalog load time. +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 option 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 them 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 `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). + +### What are possible scale implications? +As your project grows larger, keep an eye on the following aspects of your assets and bundles: +* Total bundle size - Historically Unity has not supported files larger than 4GB. This has been fixed in some recent editor versions, but there can still be issues. It is recommended to keep the content of a given bundle under this limit for best compatibility across all platforms. +* Sub assets affecting UI performance. There is no hard limit here, but if you have many assets, and those assets have many sub-assets, it may be best to turn off sub-asset display. This option only affects how the data is displayed in the Groups window, and does not affect what you can and cannot load at runtime. The option is available in the groups window under **Tools** > **Show Sprite and Subobject Addresses**. Disabling this will make the UI more responsive. +* Group hierarchy display. Another UI-only option to help with scale is **Group Hierarchy with Dashes**. This is available within the inspector of the top level settings. 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) + + diff --git a/Documentation~/LoadResourceLocations.md b/Documentation~/LoadResourceLocations.md index fa984689..65eb8162 100644 --- a/Documentation~/LoadResourceLocations.md +++ b/Documentation~/LoadResourceLocations.md @@ -68,4 +68,92 @@ IEnumerator Start() Addressables.Release(handle); } +``` + +#### Sub-Objects +Sub-Objects are a special case. Locations for Sub-Objects are generated at runtime to keep bloat out of the content catalogs and improve runtime performance, such as entering Play Mode while using the Use Asset Database Playmode script. This has implications when calling `LoadResourceLocationsAsync` with a Sub-Object key. If the system is not aware of the desired Type then IResourceLocations is generated for each `Type` of Sub-Object detected. If an AssetReference with a Sub-Object selection is not set, the system generates IResourceLocations for each Type of detected Sub-Object with the Address of the main object. + +For the following examples lets assume we have an FBX asset marked as Addressable that has a Mesh Sub-Object. + +##### When passing a string Key: +``` +IEnumerator Start() +{ + AsyncOperationHandle> handle = Addressables.LoadResourceLocationsAsync("myFBXObject"); + yield return handle; + + //This result contains 3 IResourceLocations. One with Type GameObject, one with Type Mesh, and one with Type Material. Since the string Key has no Type information we generate all possible IResourceLocations to match the request. + IList result = handle.Result; + + //... + + Addressables.Release(handle); +} +``` + +``` +IEnumerator Start() +{ + AsyncOperationHandle> handle = Addressables.LoadResourceLocationsAsync("myFBXObject[Mesh]"); + yield return handle; + + //This result contains 3 IResourceLocations. One with Type GameObject, one with Type Mesh, and one with Type Material. Since the string Key has no Type information we generate all possible IResourceLocations to match the request. + IList result = handle.Result; + + //... + + Addressables.Release(handle); +} +``` + +``` +IEnumerator Start() +{ + AsyncOperationHandle> handle = Addressables.LoadResourceLocationsAsync("myFBXObject[Mesh]", typeof(Mesh)); + yield return handle; + + //This result contains 1 IResourceLocation. Since the Type parameter has a value passed in we can create the IResourceLocation. + IList result = handle.Result; + + //... + + Addressables.Release(handle); +} +``` + +##### When using an AssetReference: +``` +//An AssetReference set to point to the Mesh Sub-Object of a FBX asset +public AssetReference myFBXMeshReference; + +IEnumerator Start() +{ + AsyncOperationHandle> handle = Addressables.LoadResourceLocationsAsync(myFBXMeshReference); + yield return handle; + + //This result contains 1 IResourceLocation. Since the AssetReference contains Type information about the Sub-Object, we can generate the appropriate IResourceLocation. + IList result = handle.Result; + + //... + + Addressables.Release(handle); +} +``` + +``` +//An AssetReference that is not set to point at a Sub-Object +public AssetReference myFBXReference; + +IEnumerator Start() +{ + AsyncOperationHandle> handle = Addressables.LoadResourceLocationsAsync(myFBXReference); + yield return handle; + + //This result contains 3 IResourceLocation. Since the AssetReference Sub-Object is not set we generate all possible IResourceLocations with the detected Sub-Object Types. + IList result = handle.Result; + + //... + + Addressables.Release(handle); +} ``` \ No newline at end of file diff --git a/Documentation~/TableOfContents.md b/Documentation~/TableOfContents.md index 36566550..6b563a3e 100644 --- a/Documentation~/TableOfContents.md +++ b/Documentation~/TableOfContents.md @@ -45,17 +45,17 @@ * [The Resource folders method](AddressableAssetsMigrationGuide.md#the-resource-folders-method) * [The AssetBundles method](AddressableAssetsMigrationGuide.md#the-assetbundles-method) * [Expanded API documentation](AddressablesAPI.md) - * [LoadingAddressableAssets](LoadingAddressableAssets.md) + * [BuildPlayerContent](BuildPlayerContent.md) + * [DownloadDependenciesAsync](DownloadDependenciesAsync.md) + * [ExceptionHandler](ExceptionHandler.md) * [InitializeAsync](InitializeAsync.md) - * [TransformInternalId](TransformInternalId.md) * [InstantiateAsync](InstantiateAsync.md) - * [DownloadDependenciesAsync](DownloadDependenciesAsync.md) * [LoadContentCatalogAsync](LoadContentCatalogAsync.md) - * [UpdateCatalogs](UpdateCatalogs.md) - * [LoadSceneAsync](LoadSceneAsync.md) - * [ExceptionHandler](ExceptionHandler.md) - * [BuildPlayerContent](BuildPlayerContent.md) + * [LoadingAddressableAssets](LoadingAddressableAssets.md) * [LoadResourceLocationsAsync](LoadResourceLocations.md) + * [LoadSceneAsync](LoadSceneAsync.md) + * [TransformInternalId](TransformInternalId.md) + * [UpdateCatalogs](UpdateCatalogs.md) * [Diagnostic Tools](DiagnosticTools.md) * [Build Layout](DiagnosticTools.md#build-layout-report) * [Build Profiling](DiagnosticTools.md#build-profiling) @@ -63,3 +63,10 @@ * [Using Analyze](DiagnosticTools.md#using-analyze) * [Provided Analyze rules](DiagnosticTools.md#provided-analyze-rules) * [Extending Analyze](DiagnosticTools.md#extending-analyze) + * [FAQ](AddressablesFAQ.md) + * [Many vs few bundles](AddressablesFAQ.md#Is-it-better-to-have-many-small-bundles-or-a-few-bigger-ones) + * [Best compression settings](AddressablesFAQ.md#What-compression-settings-are-best) + * [Minimize catlog size](AddressablesFAQ.md#Are-there-ways-to-miminize-the-catalog-size) + * [Addressables content state](AddressablesFAQ.md#What-is-addressables_content_state) + * [Scale implications](AddressablesFAQ.md#What-are-possible-scale-implications) + diff --git a/Editor/Build/AddressableAssetSettingsLocator.cs b/Editor/Build/AddressableAssetSettingsLocator.cs index 4cb86ec8..d9e4fa5f 100644 --- a/Editor/Build/AddressableAssetSettingsLocator.cs +++ b/Editor/Build/AddressableAssetSettingsLocator.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using UnityEditor.Build.Content; using UnityEngine; using UnityEngine.AddressableAssets.ResourceLocators; @@ -168,6 +169,7 @@ static void GatherEntryLocations(AddressableAssetEntry entry, Type type, IList locations) { CacheKey cacheKey = new CacheKey() { m_key = key, m_type = type }; @@ -197,8 +201,28 @@ public bool Locate(object key, Type type, out IList locations { foreach (AddressableAssetEntry e in entries) { - if (!AssetDatabase.IsValidFolder(e.AssetPath) || e.labels.Contains(key as string)) + if (AssetDatabase.IsValidFolder(e.AssetPath) && !e.labels.Contains(key as string)) + continue; + + if (type == null) + { + ObjectIdentifier[] ids = ContentBuildInterface.GetPlayerObjectIdentifiersInAsset(new GUID(e.guid), EditorUserBuildSettings.activeBuildTarget); + IEnumerable subObjectTypes = AddressableAssetEntry.GatherSubObjectTypes(ids, e.guid); + + if (subObjectTypes.Any()) + { + foreach (Type t in subObjectTypes) + GatherEntryLocations(e, t, locations, m_AddressableAssetTree); + } + else + { + GatherEntryLocations(e, null, locations, m_AddressableAssetTree); + } + } + else + { GatherEntryLocations(e, type, locations, m_AddressableAssetTree); + } } } diff --git a/Editor/Build/BuildPipelineTasks/GenerateLocationListsTask.cs b/Editor/Build/BuildPipelineTasks/GenerateLocationListsTask.cs index 935d8536..4971cda9 100644 --- a/Editor/Build/BuildPipelineTasks/GenerateLocationListsTask.cs +++ b/Editor/Build/BuildPipelineTasks/GenerateLocationListsTask.cs @@ -123,6 +123,7 @@ class BundleEntry public HashSet ExpandedDependencies; public List Assets = new List(); public AddressableAssetGroup Group; + public HashSet AssetInternalIds = new HashSet(); } static private void ExpandDependencies(BundleEntry entry) @@ -198,6 +199,7 @@ internal static Output RunInternal(Input input) foreach (BundleEntry bEntry in bundleToEntry.Values) { string assetProvider = GetAssetProviderName(bEntry.Group); + var schema = bEntry.Group.GetSchema(); foreach (GUID assetGUID in bEntry.Assets) { if (guidToEntry.TryGetValue(assetGUID.ToString(), out AddressableAssetEntry entry)) @@ -211,7 +213,7 @@ internal static Output RunInternal(Input input) else throw new Exception($"Cannot recognize file type for entry located at '{entry.AssetPath}'. Asset import failed for using an unsupported file type."); } - entry.CreateCatalogEntriesInternal(locations, true, assetProvider, bEntry.ExpandedDependencies.Select(x => x.BundleName), null, input.AssetToAssetInfo, providerTypes); + entry.CreateCatalogEntriesInternal(locations, true, assetProvider, bEntry.ExpandedDependencies.Select(x => x.BundleName), null, input.AssetToAssetInfo, providerTypes, schema.IncludeAddressInCatalog, schema.IncludeGUIDInCatalog, schema.IncludeLabelsInCatalog, bEntry.AssetInternalIds); } } } diff --git a/Editor/Build/DataBuilders/BuildScriptPackedMode.cs b/Editor/Build/DataBuilders/BuildScriptPackedMode.cs index 72cf0269..b7167206 100644 --- a/Editor/Build/DataBuilders/BuildScriptPackedMode.cs +++ b/Editor/Build/DataBuilders/BuildScriptPackedMode.cs @@ -236,7 +236,9 @@ protected virtual TResult DoBuild(AddressablesDataBuilderInput builderI } } - var contentCatalog = new ContentCatalogData(aaContext.locations, ResourceManagerRuntimeData.kCatalogAddress); + var contentCatalog = new ContentCatalogData(ResourceManagerRuntimeData.kCatalogAddress); + contentCatalog.SetData(aaContext.locations, aaContext.Settings.OptimizeCatalogSize); + contentCatalog.ResourceProviderData.AddRange(m_ResourceProviderData); foreach (var t in aaContext.providerTypes) contentCatalog.ResourceProviderData.Add(ObjectInitializationData.CreateSerializedInitializationData(t)); @@ -502,6 +504,14 @@ protected override string ProcessGroup(AddressableAssetGroup assetGroup, Address if (assetGroup == null) return string.Empty; + if (assetGroup.Schemas.Count == 0) + { + Addressables.LogWarning($"{assetGroup.Name} does not have any associated AddressableAssetGroupSchemas. " + + $"Data from this group will not be included in the build. " + + $"If this is unexpected the AddressableGroup may have become corrupted."); + return string.Empty; + } + foreach (var schema in assetGroup.Schemas) { var errorString = ProcessGroupSchema(schema, assetGroup, aaContext); @@ -713,7 +723,7 @@ internal static List PrepGroupBundlePacking(AddressableAs return combinedEntries; } - static void GenerateBuildInputDefinitions(List allEntries, List buildInputDefs, string groupGuid, string address) + internal static void GenerateBuildInputDefinitions(List allEntries, List buildInputDefs, string groupGuid, string address) { var scenes = new List(); var assets = new List(); @@ -721,7 +731,7 @@ static void GenerateBuildInputDefinitions(List allEntries { if (string.IsNullOrEmpty(e.AssetPath)) continue; - if (e.AssetPath.EndsWith(".unity")) + if (e.IsScene) scenes.Add(e); else assets.Add(e); @@ -732,17 +742,13 @@ static void GenerateBuildInputDefinitions(List allEntries buildInputDefs.Add(GenerateBuildInputDefinition(scenes, groupGuid + "_scenes_" + address + ".bundle")); } - static AssetBundleBuild GenerateBuildInputDefinition(List assets, string name) + internal static AssetBundleBuild GenerateBuildInputDefinition(List assets, string name) { + var assetInternalIds = new HashSet(); var assetsInputDef = new AssetBundleBuild(); assetsInputDef.assetBundleName = name.ToLower().Replace(" ", "").Replace('\\', '/').Replace("//", "/"); - var assetIds = new List(assets.Count); - foreach (var a in assets) - { - assetIds.Add(a.AssetPath); - } - assetsInputDef.assetNames = assetIds.ToArray(); - assetsInputDef.addressableNames = new string[0]; + assetsInputDef.assetNames = assets.Select(s => s.AssetPath).ToArray(); + assetsInputDef.addressableNames = assets.Select(s => s.GetAssetLoadPath(true, assetInternalIds)).ToArray(); return assetsInputDef; } @@ -833,11 +839,6 @@ static IList RuntimeDataBuildTasks(string builtinShaderBundleName) return buildTasks; } - static bool IsInternalIdLocal(string path) - { - return path.StartsWith("{UnityEngine.AddressableAssets.Addressables.RuntimePath}"); - } - static void CopyFileWithTimestampIfDifferent(string srcPath, string destPath, IBuildLogger log) { if (srcPath == destPath) diff --git a/Editor/Build/DataBuilders/BuildScriptVirtualMode.cs b/Editor/Build/DataBuilders/BuildScriptVirtualMode.cs index e737f48d..7c29eb34 100644 --- a/Editor/Build/DataBuilders/BuildScriptVirtualMode.cs +++ b/Editor/Build/DataBuilders/BuildScriptVirtualMode.cs @@ -232,7 +232,9 @@ TResult DoBuild(AddressablesDataBuilderInput builderInput, AddressableA } } - var contentCatalog = new ContentCatalogData(aaContext.locations, ResourceManagerRuntimeData.kCatalogAddress); + var contentCatalog = new ContentCatalogData(ResourceManagerRuntimeData.kCatalogAddress); + contentCatalog.SetData(aaContext.locations, aaContext.Settings.OptimizeCatalogSize); + contentCatalog.ResourceProviderData.AddRange(m_ResourceProviderData); foreach (var t in aaContext.providerTypes) contentCatalog.ResourceProviderData.Add(ObjectInitializationData.CreateSerializedInitializationData(t)); diff --git a/Editor/GUI/AddressableAssetSettingsInspector.cs b/Editor/GUI/AddressableAssetSettingsInspector.cs index 5ca94aee..042d1867 100644 --- a/Editor/GUI/AddressableAssetSettingsInspector.cs +++ b/Editor/GUI/AddressableAssetSettingsInspector.cs @@ -98,6 +98,8 @@ void OnEnable() 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."); GUIContent m_BundleLocalCatalog = new GUIContent("Compress Local Catalog", "If set, the local content catalog will be compressed in an asset bundle. This will affect build and load time of catalog. We recommend disabling this during iteration."); + GUIContent m_OptimizeCatalogSize = + new GUIContent("Optimize Catalog Size", "If set, duplicate internal ids will be extracted to a lookup table and reconstructed at runtime. This can reduce the size of the catalog but may impact performance due to extra processing at load time."); GUIContent m_CheckForCatalogUpdateOnInit = new GUIContent("Disable Catalog Update on Startup", "If set, this will forgo checking for content catalog updates on initialization."); GUIContent m_RemoteCatBuildPath = @@ -178,6 +180,10 @@ public override void OnInspectorGUI() bool bundleLocalCatalog = EditorGUILayout.Toggle(m_BundleLocalCatalog, m_AasTarget.BundleLocalCatalog); if (bundleLocalCatalog != m_AasTarget.BundleLocalCatalog) m_QueuedChanges.Add(() => m_AasTarget.BundleLocalCatalog = bundleLocalCatalog); + bool optimizeCatalogSize = EditorGUILayout.Toggle(m_OptimizeCatalogSize, m_AasTarget.OptimizeCatalogSize); + if (optimizeCatalogSize != m_AasTarget.OptimizeCatalogSize) + m_QueuedChanges.Add(() => m_AasTarget.OptimizeCatalogSize = optimizeCatalogSize); + bool buildRemoteCatalog = EditorGUILayout.Toggle(m_BuildRemoteCatalog, m_AasTarget.BuildRemoteCatalog); if (buildRemoteCatalog != m_AasTarget.BuildRemoteCatalog) m_QueuedChanges.Add(() => m_AasTarget.BuildRemoteCatalog = buildRemoteCatalog); diff --git a/Editor/GUI/AddressableAssetsSettingsGroupEditor.cs b/Editor/GUI/AddressableAssetsSettingsGroupEditor.cs index f0e9d107..7de8134b 100644 --- a/Editor/GUI/AddressableAssetsSettingsGroupEditor.cs +++ b/Editor/GUI/AddressableAssetsSettingsGroupEditor.cs @@ -345,16 +345,22 @@ public void OnEnable() { if (AddressableAssetSettingsDefaultObject.Settings == null) return; - AddressableAssetSettingsDefaultObject.Settings.OnModification += OnSettingsModification; - m_ModificationRegistered = true; + if (!m_ModificationRegistered) + { + AddressableAssetSettingsDefaultObject.Settings.OnModification += OnSettingsModification; + m_ModificationRegistered = true; + } } public void OnDisable() { if (AddressableAssetSettingsDefaultObject.Settings == null) return; - AddressableAssetSettingsDefaultObject.Settings.OnModification -= OnSettingsModification; - m_ModificationRegistered = false; + if (m_ModificationRegistered) + { + AddressableAssetSettingsDefaultObject.Settings.OnModification -= OnSettingsModification; + m_ModificationRegistered = false; + } } public bool OnGUI(Rect pos) @@ -362,14 +368,6 @@ public bool OnGUI(Rect pos) if (settings == null) return false; - if (!m_ModificationRegistered) - { - m_ModificationRegistered = true; - settings.OnModification -= OnSettingsModification; //just in case... - settings.OnModification += OnSettingsModification; - } - - if (m_EntryTree == null) { if (m_TreeState == null) diff --git a/Editor/GUI/AddressableAssetsSettingsGroupTreeView.cs b/Editor/GUI/AddressableAssetsSettingsGroupTreeView.cs index cee69a18..2620c128 100644 --- a/Editor/GUI/AddressableAssetsSettingsGroupTreeView.cs +++ b/Editor/GUI/AddressableAssetsSettingsGroupTreeView.cs @@ -8,6 +8,7 @@ using UnityEngine.Assertions; using UnityEngine.AddressableAssets; using Debug = UnityEngine.Debug; +using static UnityEditor.AddressableAssets.Settings.AddressablesFileEnumeration; namespace UnityEditor.AddressableAssets.GUI { @@ -98,9 +99,11 @@ protected override void SelectionChanged(IList selectedIds) protected override TreeViewItem BuildRoot() { var root = new TreeViewItem(-1, -1); - foreach (var group in m_Editor.settings.groups) - AddGroupChildrenBuild(group, root); - + using (new AddressablesFileEnumerationScope(BuildAddressableTree(m_Editor.settings))) + { + foreach (var group in m_Editor.settings.groups) + AddGroupChildrenBuild(group, root); + } return root; } diff --git a/Editor/GUI/AddressableAssetsWindow.cs b/Editor/GUI/AddressableAssetsWindow.cs index 06d56361..3fd7eaef 100644 --- a/Editor/GUI/AddressableAssetsWindow.cs +++ b/Editor/GUI/AddressableAssetsWindow.cs @@ -47,9 +47,7 @@ public static Vector2 GetWindowPosition() public void OnEnable() { - if (m_GroupEditor != null) - m_GroupEditor.OnEnable(); - + m_GroupEditor?.OnEnable(); if (m_Request == null || m_Request.Status == StatusCode.Failure) { m_Request = PackageManager.Client.Search("com.unity.addressables"); @@ -58,8 +56,7 @@ public void OnEnable() public void OnDisable() { - if (m_GroupEditor != null) - m_GroupEditor.OnDisable(); + m_GroupEditor?.OnDisable(); } internal void OfferToConvert(AddressableAssetSettings settings) @@ -149,10 +146,8 @@ public void OnGUI() Rect contentRect = new Rect(0, 0, position.width, position.height); if (m_GroupEditor == null) - { m_GroupEditor = new AddressableAssetsSettingsGroupEditor(this); - m_GroupEditor.OnEnable(); - } + if (m_GroupEditor.OnGUI(contentRect)) Repaint(); } diff --git a/Editor/GUI/AssetInspectorGUI.cs b/Editor/GUI/AssetInspectorGUI.cs index 2361c0b1..718cedf5 100644 --- a/Editor/GUI/AssetInspectorGUI.cs +++ b/Editor/GUI/AssetInspectorGUI.cs @@ -54,9 +54,12 @@ static void SetAaEntry(AddressableAssetSettings aaSettings, Object[] targets, bo else { var resourceTargets = targetInfos.Where(ti => AddressableAssetUtility.IsInResources(ti.Path)); - var resourcePaths = resourceTargets.Select(t => t.Path).ToList(); - var resourceGuids = resourceTargets.Select(t => t.Guid).ToList(); - AddressableAssetUtility.SafeMoveResourcesToGroup(aaSettings, aaSettings.DefaultGroup, resourcePaths, resourceGuids); + if (resourceTargets.Any()) + { + var resourcePaths = resourceTargets.Select(t => t.Path).ToList(); + var resourceGuids = resourceTargets.Select(t => t.Guid).ToList(); + AddressableAssetUtility.SafeMoveResourcesToGroup(aaSettings, aaSettings.DefaultGroup, resourcePaths, resourceGuids); + } var entriesAdded = new List(); var modifiedGroups = new HashSet(); diff --git a/Editor/GUI/AssetReferenceDrawer.cs b/Editor/GUI/AssetReferenceDrawer.cs index bfa9c683..3eceef2f 100644 --- a/Editor/GUI/AssetReferenceDrawer.cs +++ b/Editor/GUI/AssetReferenceDrawer.cs @@ -484,9 +484,10 @@ private Rect DrawSubAssetsControl(SerializedProperty property, List subA var objName = subAsset == null ? "" : subAsset.name; if (objName.EndsWith("(Clone)")) objName = objName.Replace("(Clone)", ""); - objNames[i] = objName; + objNames[i] = subAsset == null ? objName : $"{objName}:{subAsset.GetType()}"; - if (m_AssetRefObject.SubObjectName == objName) + if (subAsset == null || m_AssetRefObject.SubObjectName == objName && + subAsset.GetType().AssemblyQualifiedName == m_AssetRefObject.SubOjbectType.AssemblyQualifiedName) selIndex = i; } @@ -547,7 +548,7 @@ internal bool SetSubAssets(SerializedProperty property, Object subAsset, FieldIn { bool valueChanged = false; string spriteName = null; - if (subAsset != null) + if (subAsset != null && subAsset.GetType() == typeof(Sprite)) { spriteName = subAsset.name; if (spriteName.EndsWith("(Clone)")) diff --git a/Editor/Settings/AddressableAssetEntry.cs b/Editor/Settings/AddressableAssetEntry.cs index e8ef0ad3..875f729b 100644 --- a/Editor/Settings/AddressableAssetEntry.cs +++ b/Editor/Settings/AddressableAssetEntry.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; using System.Runtime.Serialization.Formatters.Binary; using UnityEditor.Build.Content; using UnityEditor.PackageManager; @@ -224,12 +225,19 @@ public bool SetLabel(string label, bool enable, bool force = false, bool postEve /// Creates a list of keys that can be used to load this entry. /// /// The list of keys. This will contain the address, the guid as a Hash128 if valid, all assigned labels, and the scene index if applicable. - public List CreateKeyList() + public List CreateKeyList() => CreateKeyList(true, true, true); + + /// + /// Creates a list of keys that can be used to load this entry. + /// + /// The list of keys. This will contain the address, the guid as a Hash128 if valid, all assigned labels, and the scene index if applicable. + internal List CreateKeyList(bool includeAddress, bool includeGUID, bool includeLabels) { var keys = new List(); //the address must be the first key - keys.Add(address); - if (!string.IsNullOrEmpty(guid)) + if (includeAddress) + keys.Add(address); + if (includeGUID && !string.IsNullOrEmpty(guid)) keys.Add(guid); if (IsScene && IsInSceneList) { @@ -238,7 +246,7 @@ public List CreateKeyList() keys.Add(index); } - if (labels != null && labels.Count > 0) + if (includeLabels && labels != null && labels.Count > 0) { var labelsToRemove = new HashSet(); var currentLabels = parentGroup.Settings.GetLabels(); @@ -299,6 +307,11 @@ internal void SetCachedPath(string newCachedPath) } } + internal void SetSubObjectType(Type type) + { + m_cachedMainAssetType = type; + } + internal string m_cachedAssetPath = null; /// /// The path of the asset. @@ -392,25 +405,40 @@ public UnityEngine.Object TargetAsset return m_TargetAsset; } } - /// /// The asset load path. This is used to determine the internal id of resource locations. /// /// True if the asset will be contained in an asset bundle. /// Return the runtime path that should be used to load this entry. public string GetAssetLoadPath(bool isBundled) + { + return GetAssetLoadPath(isBundled, null); + } + + /// + /// The asset load path. This is used to determine the internal id of resource locations. + /// + /// True if the asset will be contained in an asset bundle. + /// Return the runtime path that should be used to load this entry. + internal string GetAssetLoadPath(bool isBundled, HashSet otherLoadPaths) { if (!IsScene) { if (IsInResources) + { return GetResourcesPath(AssetPath); + } else + { + if (isBundled) + return parentGroup.GetSchema().GetAssetLoadPath(AssetPath, otherLoadPaths, p => guid); return AssetPath; + } } else { if (isBundled) - return AssetPath; + return parentGroup.GetSchema().GetAssetLoadPath(AssetPath, otherLoadPaths, p => guid); var path = AssetPath; int i = path.LastIndexOf(".unity"); if (i > 0) @@ -444,6 +472,9 @@ static string GetResourcesPath(string path) /// Optional predicate to run against each entry, only returning those that pass. A null filter will return all entries public void GatherAllAssets(List assets, bool includeSelf, bool recurseAll, bool includeSubObjects, Func entryFilter = null) { + if (assets == null) + assets = new List(); + if (guid == EditorSceneListName) { GatherEditorSceneEntries(assets, entryFilter); @@ -509,6 +540,7 @@ void GatherSubObjectEntries(List assets, string path) newEntry.IsSubAsset = true; newEntry.ParentEntry = this; newEntry.IsInResources = IsInResources; + newEntry.SetSubObjectType(o.GetType()); assets.Add(newEntry); } } @@ -614,7 +646,7 @@ internal void GatherResourcesEntries(List assets, bool re var pd = parentGroup.GetSchema(); if (pd.IncludeResourcesFolders) { - foreach (var resourcesDir in Directory.GetDirectories("Assets", "Resources", SearchOption.AllDirectories)) + foreach (var resourcesDir in GetResourceDirectories()) { foreach (var file in Directory.GetFiles(resourcesDir, "*.*", recurseAll ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)) { @@ -655,6 +687,40 @@ internal void GatherResourcesEntries(List assets, bool re } } + static IEnumerable GetResourceDirectories() + { + foreach (string path in GetResourceDirectoriesatPath("Assets")) + { + yield return path; + } + + List packages = AddressableAssetUtility.GetUserPackages(); + foreach (PackageManager.PackageInfo package in packages) + { + foreach (string path in GetResourceDirectoriesatPath(package.assetPath)) + { + yield return path; + } + } + } + + static IEnumerable GetResourceDirectoriesatPath(string rootPath) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + foreach (string dir in Directory.EnumerateDirectories(rootPath, "*", SearchOption.AllDirectories)) + { + if (dir.EndsWith("/resources", StringComparison.OrdinalIgnoreCase)) + yield return dir; + } + } + else + { + foreach (string dir in Directory.EnumerateDirectories(rootPath, "Resources", SearchOption.AllDirectories)) + yield return dir; + } + } + void GatherEditorSceneEntries(List assets, Func entryFilter) { var settings = parentGroup.Settings; @@ -678,29 +744,6 @@ void GatherEditorSceneEntries(List assets, Func GetAllResourcesFolders() - { - foreach (string assetDir in Directory.GetDirectories("Assets", "Resources", SearchOption.AllDirectories)) - { - yield return assetDir; - } - - ListRequest req = Client.List(); - while (!req.IsCompleted) {} - - if (req.Status == StatusCode.Success) - { - PackageCollection collection = req.Result; - foreach (PackageManager.PackageInfo package in collection) - { - if (package.name.StartsWith("com.unity.")) - continue; - foreach (string assetDir in Directory.GetDirectories(package.assetPath, "Resources", SearchOption.AllDirectories)) - yield return assetDir; - } - } - } - internal void GatherAllAssetReferenceDrawableEntries(List refEntries, AddressableAssetSettings settings) { var path = AssetPath; @@ -794,16 +837,19 @@ public override string ToString() /// Any unknown provider types are added to this set in order to ensure they are not stripped. public void CreateCatalogEntries(List entries, bool isBundled, string providerType, IEnumerable dependencies, object extraData, HashSet providerTypes) { - CreateCatalogEntriesInternal(entries, isBundled, providerType, dependencies, extraData, null, providerTypes); + CreateCatalogEntriesInternal(entries, isBundled, providerType, dependencies, extraData, null, providerTypes, true, true, true, null); } - internal void CreateCatalogEntriesInternal(List entries, bool isBundled, string providerType, IEnumerable dependencies, object extraData, Dictionary depInfo, HashSet providerTypes) + internal void CreateCatalogEntriesInternal(List entries, bool isBundled, string providerType, IEnumerable dependencies, object extraData, Dictionary depInfo, HashSet providerTypes, bool includeAddress, bool includeGUID, bool includeLabels, HashSet assetsInBundle) { if (string.IsNullOrEmpty(AssetPath)) return; - string assetPath = GetAssetLoadPath(isBundled); - List keyList = CreateKeyList(); + string assetPath = GetAssetLoadPath(isBundled, assetsInBundle); + List keyList = CreateKeyList(includeAddress, includeGUID, includeLabels); + if (keyList.Count == 0) + return; + Type mainType = AddressableAssetUtility.MapEditorTypeToRuntimeType(MainAssetType, false); if (mainType == null && !IsInResources) { diff --git a/Editor/Settings/AddressableAssetSettings.cs b/Editor/Settings/AddressableAssetSettings.cs index bfa08c24..9949b58a 100644 --- a/Editor/Settings/AddressableAssetSettings.cs +++ b/Editor/Settings/AddressableAssetSettings.cs @@ -295,6 +295,9 @@ public string GroupTemplateFolder /// public bool IsPersisted { get { return !m_IsTemporary; } } + [SerializeField] + bool m_OptimizeCatalogSize = false; + [SerializeField] bool m_BuildRemoteCatalog = false; @@ -344,6 +347,15 @@ public bool ContiguousBundles set { m_ContiguousBundles = value; } } + /// + /// Enables size optimization of content catalogs. This may increase the cpu usage of loading the catalog. + /// + internal bool OptimizeCatalogSize + { + get { return m_OptimizeCatalogSize; } + set { m_OptimizeCatalogSize = value; } + } + /// /// Determine if a remote catalog should be built-for and loaded-by the app. /// @@ -908,8 +920,8 @@ public List GetLabels() /// Send modification event. public void AddLabel(string label, bool postEvent = true) { - m_LabelTable.AddLabelName(label); - SetDirty(ModificationEvent.LabelAdded, label, postEvent, true); + if(m_LabelTable.AddLabelName(label)) + SetDirty(ModificationEvent.LabelAdded, label, postEvent, true); } /// @@ -1680,16 +1692,15 @@ internal void RemoveGroupInternal(AddressableAssetGroup g, bool deleteAsset, boo internal void SetLabelValueForEntries(List entries, string label, bool value, bool postEvent = true) { - if (value) - AddLabel(label); + var addedNewLabel = value && m_LabelTable.AddLabelName(label); foreach (var e in entries) { - e.SetLabel(label, value, false); + e.SetLabel(label, value, false, false); AddressableAssetUtility.OpenAssetIfUsingVCIntegration(e.parentGroup); } - SetDirty(ModificationEvent.EntryModified, entries, postEvent, true); + SetDirty(ModificationEvent.EntryModified, entries, postEvent, addedNewLabel); AddressableAssetUtility.OpenAssetIfUsingVCIntegration(this); } diff --git a/Editor/Settings/AddressableAssetUtility.cs b/Editor/Settings/AddressableAssetUtility.cs index 40c35e4f..02573dd4 100644 --- a/Editor/Settings/AddressableAssetUtility.cs +++ b/Editor/Settings/AddressableAssetUtility.cs @@ -5,6 +5,8 @@ using System.Reflection; using UnityEditor.AddressableAssets.Settings.GroupSchemas; using UnityEditor.Build.Utilities; +using UnityEditor.PackageManager; +using UnityEditor.PackageManager.Requests; using UnityEditor.VersionControl; using UnityEngine; @@ -39,22 +41,18 @@ internal static bool GetPathAndGUIDFromTarget(Object target, out string path, ou return false; return true; } - + static HashSet excludedExtensions = new HashSet(new string[] { ".cs", ".js", ".boo", ".exe", ".dll", ".meta" }); internal static bool IsPathValidForEntry(string path) { if (string.IsNullOrEmpty(path)) return false; - path = path.ToLower(); - if (!path.StartsWith("assets") && !IsPathValidPackageAsset(path)) + if (!path.StartsWith("assets", StringComparison.OrdinalIgnoreCase) && !IsPathValidPackageAsset(path)) return false; if (path == CommonStrings.UnityEditorResourcePath || path == CommonStrings.UnityDefaultResourcePath || path == CommonStrings.UnityBuiltInExtraPath) return false; - string ext = Path.GetExtension(path); - if (ext == ".cs" || ext == ".js" || ext == ".boo" || ext == ".exe" || ext == ".dll" || ext == ".meta") - return false; - return true; + return !excludedExtensions.Contains(Path.GetExtension(path)); } internal static bool IsPathValidPackageAsset(string path) @@ -66,7 +64,7 @@ internal static bool IsPathValidPackageAsset(string path) return false; if (splitPath[0] != "packages") return false; - if (splitPath[1].StartsWith("com.unity.")) + if (splitPath[1].StartsWith("com.unity.", StringComparison.Ordinal)) return false; if (splitPath.Length == 3) { @@ -333,5 +331,24 @@ internal static bool OpenAssetIfUsingVCIntegration(Object target, string path, b GUIUtility.ExitGUI(); return openedAsset; } + + internal static List GetUserPackages() + { + ListRequest req = Client.List(); + while (!req.IsCompleted) {} + + var packages = new List(); + if (req.Status == StatusCode.Success) + { + PackageCollection collection = req.Result; + foreach (PackageManager.PackageInfo package in collection) + { + if (package.name.StartsWith("com.unity.")) + continue; + packages.Add(package); + } + } + return packages; + } } } diff --git a/Editor/Settings/AddressablesFileEnumeration.cs b/Editor/Settings/AddressablesFileEnumeration.cs index 076dbc73..56699d26 100644 --- a/Editor/Settings/AddressablesFileEnumeration.cs +++ b/Editor/Settings/AddressablesFileEnumeration.cs @@ -153,7 +153,7 @@ static void AddLocalFilesToTreeIfNotEnumerated(AddressableAssetTree tree, string continue; string convertedPath = filename.Replace('\\', '/'); var node = tree.FindNode(convertedPath, true); - node.IsFolder = Directory.Exists(filename); + node.IsFolder = AssetDatabase.IsValidFolder(filename); node.HasEnumerated = true; } } @@ -206,7 +206,7 @@ static void EndPrecomputedEnumerationSession() public static List EnumerateAddressableFolder(string path, AddressableAssetSettings settings, bool recurseAll, IBuildLogger logger = null) { - if(!Directory.Exists(path)) + if(!AssetDatabase.IsValidFolder(path)) throw new Exception($"Path {path} cannot be enumerated because it does not exist"); AddressableAssetTree tree = m_PrecomputedTree != null ? m_PrecomputedTree : BuildAddressableTree(settings, logger); diff --git a/Editor/Settings/GroupSchemas/BundledAssetGroupSchema.cs b/Editor/Settings/GroupSchemas/BundledAssetGroupSchema.cs index 2a653006..9f1481b8 100644 --- a/Editor/Settings/GroupSchemas/BundledAssetGroupSchema.cs +++ b/Editor/Settings/GroupSchemas/BundledAssetGroupSchema.cs @@ -68,6 +68,77 @@ public BundleCompressionMode Compression List m_QueuedChanges = new List(); + /// + /// Options for internal id of assets in bundles. + /// + internal enum AssetNamingMode + { + /// + /// Use to identify assets by their full path. + /// + FullPath, + /// + /// Use to identify assets by their filename only. There is a risk of collisions when assets in different folders have the same filename. + /// + Filename, + /// + /// Use to identify assets by their asset guid. This will save space over using the full path and will be stable if assets move in the project. + /// + GUID, + /// + /// This method attempts to use the smallest identifier for internal asset ids. For asset bundles with very few items, this can save a significant amount of space in the content catalog. + /// + Dynamic + } + + [SerializeField] + bool m_IncludeAddressInCatalog = true; + [SerializeField] + bool m_IncludeGUIDInCatalog = true; + [SerializeField] + bool m_IncludeLabelsInCatalog = true; + + /// + /// If enabled, addresses are included in the content catalog. This is required if assets are to be loaded via their main address. + /// + internal bool IncludeAddressInCatalog + { + get => m_IncludeAddressInCatalog; + set => m_IncludeAddressInCatalog = value; + } + + /// + /// If enabled, guids are included in content catalogs. This is required if assets are to be loaded via AssetReference. + /// + internal bool IncludeGUIDInCatalog + { + get => m_IncludeGUIDInCatalog; + set => m_IncludeGUIDInCatalog = value; + } + + /// + /// If enabled, labels are included in the content catalogs. This is required if labels are used at runtime load load assets. + /// + internal bool IncludeLabelsInCatalog + { + get => m_IncludeLabelsInCatalog; + set => m_IncludeLabelsInCatalog = value; + } + + /// + /// Internal Id mode for assets in bundles. + /// + internal AssetNamingMode InternalIdNamingMode + { + get { return m_InternalIdNamingMode; } + set { m_InternalIdNamingMode = value; } + } + + [SerializeField] + [Tooltip("Indicates how the internal asset name will be generated.")] + AssetNamingMode m_InternalIdNamingMode = AssetNamingMode.FullPath; + + /// /// Gets the build compression settings for bundles in this group. /// @@ -146,6 +217,7 @@ public bool UseAssetBundleCache [SerializeField] [Tooltip("If true, the CRC (Cyclic Redundancy Check) of the asset bundle is used to check the integrity. This can be used for both local and remote bundles.")] bool m_UseAssetBundleCrc = true; + /// /// If true, the CRC and Hash values of the asset bundle are used to determine if a bundle can be loaded from the local cache instead of downloaded. /// @@ -300,6 +372,29 @@ protected override void OnSetGroup(AddressableAssetGroup group) m_BundledAssetProviderType.Value = typeof(BundledAssetProvider); } + internal string GetAssetLoadPath(string assetPath, HashSet otherLoadPaths, Func pathToGUIDFunc) + { + switch (InternalIdNamingMode) + { + case AssetNamingMode.FullPath: return assetPath; + case AssetNamingMode.Filename: return System.IO.Path.GetFileName(assetPath); + case AssetNamingMode.GUID: return pathToGUIDFunc(assetPath); + case AssetNamingMode.Dynamic: + { + var g = pathToGUIDFunc(assetPath); + if (otherLoadPaths == null) + return g; + var len = 1; + var p = g.Substring(0, len); + while (otherLoadPaths.Contains(p)) + p = g.Substring(0, ++len); + otherLoadPaths.Add(p); + return p; + } + } + return assetPath; + } + /// /// Impementation of ISerializationCallbackReceiver, does nothing. /// @@ -425,7 +520,7 @@ public override void ShowAllProperties() } GUIContent m_AssetProviderContent = new GUIContent("Asset Provider", "The provider to use for loading assets out of AssetBundles"); - GUIContent m_BundleProviderContent = new GUIContent("AssetBundle Provider", "The provider to use for loading AssetBundles (not the assets within bundles)"); + GUIContent m_BundleProviderContent = new GUIContent("Asset Bundle Provider", "The provider to use for loading AssetBundles (not the assets within bundles)"); /// public override void OnGUI() @@ -435,27 +530,31 @@ public override void OnGUI() m_ShowPaths = EditorGUILayout.Foldout(m_ShowPaths, "Build and Load Paths"); if (m_ShowPaths) { - EditorGUILayout.PropertyField(so.FindProperty("m_BuildPath"), true); - EditorGUILayout.PropertyField(so.FindProperty("m_LoadPath"), true); + EditorGUILayout.PropertyField(so.FindProperty(nameof(m_BuildPath)), true); + EditorGUILayout.PropertyField(so.FindProperty(nameof(m_LoadPath)), true); } m_ShowAdvanced = EditorGUILayout.Foldout(m_ShowAdvanced, "Advanced Options"); if (m_ShowAdvanced) { - EditorGUILayout.PropertyField(so.FindProperty("m_Compression"), true); - EditorGUILayout.PropertyField(so.FindProperty("m_IncludeInBuild"), true); - EditorGUILayout.PropertyField(so.FindProperty("m_ForceUniqueProvider"), true); - EditorGUILayout.PropertyField(so.FindProperty("m_UseAssetBundleCache"), true); - EditorGUILayout.PropertyField(so.FindProperty("m_UseAssetBundleCrc"), true); - EditorGUILayout.PropertyField(so.FindProperty("m_UseAssetBundleCrcForCachedBundles"), true); - EditorGUILayout.PropertyField(so.FindProperty("m_Timeout"), true); - EditorGUILayout.PropertyField(so.FindProperty("m_ChunkedTransfer"), true); - EditorGUILayout.PropertyField(so.FindProperty("m_RedirectLimit"), true); - EditorGUILayout.PropertyField(so.FindProperty("m_RetryCount"), true); - EditorGUILayout.PropertyField(so.FindProperty("m_BundleMode"), true); - EditorGUILayout.PropertyField(so.FindProperty("m_BundleNaming"), true); - EditorGUILayout.PropertyField(so.FindProperty("m_BundledAssetProviderType"), m_AssetProviderContent, true); - EditorGUILayout.PropertyField(so.FindProperty("m_AssetBundleProviderType"), m_BundleProviderContent, true); + EditorGUILayout.PropertyField(so.FindProperty(nameof(m_Compression)), true); + EditorGUILayout.PropertyField(so.FindProperty(nameof(m_IncludeInBuild)), true); + EditorGUILayout.PropertyField(so.FindProperty(nameof(m_ForceUniqueProvider)), true); + EditorGUILayout.PropertyField(so.FindProperty(nameof(m_UseAssetBundleCache)), true); + EditorGUILayout.PropertyField(so.FindProperty(nameof(m_UseAssetBundleCrc)), true); + EditorGUILayout.PropertyField(so.FindProperty(nameof(m_UseAssetBundleCrcForCachedBundles)), true); + EditorGUILayout.PropertyField(so.FindProperty(nameof(m_Timeout)), true); + EditorGUILayout.PropertyField(so.FindProperty(nameof(m_ChunkedTransfer)), true); + EditorGUILayout.PropertyField(so.FindProperty(nameof(m_RedirectLimit)), true); + EditorGUILayout.PropertyField(so.FindProperty(nameof(m_RetryCount)), true); + EditorGUILayout.PropertyField(so.FindProperty(nameof(m_IncludeAddressInCatalog)), true); + EditorGUILayout.PropertyField(so.FindProperty(nameof(m_IncludeGUIDInCatalog)), true); + EditorGUILayout.PropertyField(so.FindProperty(nameof(m_IncludeLabelsInCatalog)), true); + EditorGUILayout.PropertyField(so.FindProperty(nameof(m_BundleMode)), true); + EditorGUILayout.PropertyField(so.FindProperty(nameof(m_InternalIdNamingMode)), true); + EditorGUILayout.PropertyField(so.FindProperty(nameof(m_BundleNaming)), true); + EditorGUILayout.PropertyField(so.FindProperty(nameof(m_BundledAssetProviderType)), m_AssetProviderContent, true); + EditorGUILayout.PropertyField(so.FindProperty(nameof(m_AssetBundleProviderType)), m_BundleProviderContent, true); } so.ApplyModifiedProperties(); @@ -486,9 +585,9 @@ public override void OnGUIMultiple(List otherSchema if (m_ShowPaths) { // BuildPath - prop = so.FindProperty("m_BuildPath"); + prop = so.FindProperty(nameof(m_BuildPath)); var prevBuildPath = m_BuildPath.Id; - ShowMixedValue(prop, otherSchemas, typeof(ProfileValueReference), "m_BuildPath"); + ShowMixedValue(prop, otherSchemas, typeof(ProfileValueReference), nameof(m_BuildPath)); EditorGUI.BeginChangeCheck(); EditorGUILayout.PropertyField(prop, true); @@ -507,9 +606,9 @@ public override void OnGUIMultiple(List otherSchema EditorGUI.showMixedValue = false; // LoadPath - prop = so.FindProperty("m_LoadPath"); + prop = so.FindProperty(nameof(m_LoadPath)); var prevLoadPath = m_LoadPath.Id; - ShowMixedValue(prop, otherSchemas, typeof(ProfileValueReference), "m_LoadPath"); + ShowMixedValue(prop, otherSchemas, typeof(ProfileValueReference), nameof(m_LoadPath)); EditorGUI.BeginChangeCheck(); EditorGUILayout.PropertyField(prop, true); @@ -540,8 +639,8 @@ public override void OnGUIMultiple(List otherSchema if (m_ShowAdvanced) { // Compression - prop = so.FindProperty("m_Compression"); - ShowMixedValue(prop, otherSchemas, typeof(Enum), "m_Compression"); + prop = so.FindProperty(nameof(m_Compression)); + ShowMixedValue(prop, otherSchemas, typeof(Enum), nameof(m_Compression)); EditorGUI.BeginChangeCheck(); BundleCompressionMode newCompression = (BundleCompressionMode)EditorGUILayout.EnumPopup(prop.displayName, Compression); if (EditorGUI.EndChangeCheck()) @@ -556,8 +655,8 @@ public override void OnGUIMultiple(List otherSchema EditorGUI.showMixedValue = false; // IncludeInBuild - prop = so.FindProperty("m_IncludeInBuild"); - ShowMixedValue(prop, otherSchemas, typeof(bool), "m_IncludeInBuild"); + prop = so.FindProperty(nameof(m_IncludeInBuild)); + ShowMixedValue(prop, otherSchemas, typeof(bool), nameof(m_IncludeInBuild)); EditorGUI.BeginChangeCheck(); bool newIncludeInBuild = (bool)EditorGUILayout.Toggle(prop.displayName, IncludeInBuild); if (EditorGUI.EndChangeCheck()) @@ -570,8 +669,8 @@ public override void OnGUIMultiple(List otherSchema EditorGUI.showMixedValue = false; // ForceUniqueProvider - prop = so.FindProperty("m_ForceUniqueProvider"); - ShowMixedValue(prop, otherSchemas, typeof(bool), "m_ForceUniqueProvider"); + prop = so.FindProperty(nameof(m_ForceUniqueProvider)); + ShowMixedValue(prop, otherSchemas, typeof(bool), nameof(m_ForceUniqueProvider)); EditorGUI.BeginChangeCheck(); bool newForceUniqueProvider = (bool)EditorGUILayout.Toggle(prop.displayName, ForceUniqueProvider); if (EditorGUI.EndChangeCheck()) @@ -584,8 +683,8 @@ public override void OnGUIMultiple(List otherSchema EditorGUI.showMixedValue = false; // UseAssetBundleCache - prop = so.FindProperty("m_UseAssetBundleCache"); - ShowMixedValue(prop, otherSchemas, typeof(bool), "m_UseAssetBundleCache"); + prop = so.FindProperty(nameof(m_UseAssetBundleCache)); + ShowMixedValue(prop, otherSchemas, typeof(bool), nameof(m_UseAssetBundleCache)); EditorGUI.BeginChangeCheck(); bool newUseAssetBundleCache = (bool)EditorGUILayout.Toggle(prop.displayName, UseAssetBundleCache); if (EditorGUI.EndChangeCheck()) @@ -598,8 +697,8 @@ public override void OnGUIMultiple(List otherSchema EditorGUI.showMixedValue = false; // UseAssetBundleCrc - prop = so.FindProperty("m_UseAssetBundleCrc"); - ShowMixedValue(prop, otherSchemas, typeof(bool), "m_UseAssetBundleCrc"); + prop = so.FindProperty(nameof(m_UseAssetBundleCrc)); + ShowMixedValue(prop, otherSchemas, typeof(bool), nameof(m_UseAssetBundleCrc)); EditorGUI.BeginChangeCheck(); bool newUseAssetBundleCrc = (bool)EditorGUILayout.Toggle(prop.displayName, UseAssetBundleCrc); if (EditorGUI.EndChangeCheck()) @@ -612,8 +711,8 @@ public override void OnGUIMultiple(List otherSchema EditorGUI.showMixedValue = false; //UseAssetBundleCrcForCachedBundles - prop = so.FindProperty("m_UseAssetBundleCrcForCachedBundles"); - ShowMixedValue(prop, otherSchemas, typeof(bool), "m_UseAssetBundleCrcForCachedBundles"); + prop = so.FindProperty(nameof(m_UseAssetBundleCrcForCachedBundles)); + ShowMixedValue(prop, otherSchemas, typeof(bool), nameof(m_UseAssetBundleCrcForCachedBundles)); EditorGUI.BeginChangeCheck(); bool newUseAssetBundleCrcForCache = (bool)EditorGUILayout.Toggle(prop.displayName, UseAssetBundleCrcForCachedBundles); if (EditorGUI.EndChangeCheck()) @@ -626,8 +725,8 @@ public override void OnGUIMultiple(List otherSchema EditorGUI.showMixedValue = false; // Timeout - prop = so.FindProperty("m_Timeout"); - ShowMixedValue(prop, otherSchemas, typeof(int), "m_Timeout"); + prop = so.FindProperty(nameof(m_Timeout)); + ShowMixedValue(prop, otherSchemas, typeof(int), nameof(m_Timeout)); EditorGUI.BeginChangeCheck(); int newTimeout = (int)EditorGUILayout.IntField(prop.displayName, Timeout); if (EditorGUI.EndChangeCheck()) @@ -641,8 +740,8 @@ public override void OnGUIMultiple(List otherSchema EditorGUI.showMixedValue = false; // ChunkedTransfer - prop = so.FindProperty("m_ChunkedTransfer"); - ShowMixedValue(prop, otherSchemas, typeof(bool), "m_ChunkedTransfer"); + prop = so.FindProperty(nameof(m_ChunkedTransfer)); + ShowMixedValue(prop, otherSchemas, typeof(bool), nameof(m_ChunkedTransfer)); EditorGUI.BeginChangeCheck(); bool newChunkedTransfer = (bool)EditorGUILayout.Toggle(prop.displayName, ChunkedTransfer); if (EditorGUI.EndChangeCheck()) @@ -655,8 +754,8 @@ public override void OnGUIMultiple(List otherSchema EditorGUI.showMixedValue = false; // RedirectLimit - prop = so.FindProperty("m_RedirectLimit"); - ShowMixedValue(prop, otherSchemas, typeof(int), "m_RedirectLimit"); + prop = so.FindProperty(nameof(m_RedirectLimit)); + ShowMixedValue(prop, otherSchemas, typeof(int), nameof(m_RedirectLimit)); EditorGUI.BeginChangeCheck(); int newRedirectLimit = (int)EditorGUILayout.IntField(prop.displayName, RedirectLimit); if (EditorGUI.EndChangeCheck()) @@ -669,8 +768,8 @@ public override void OnGUIMultiple(List otherSchema EditorGUI.showMixedValue = false; // RetryCount - prop = so.FindProperty("m_RetryCount"); - ShowMixedValue(prop, otherSchemas, typeof(int), "m_RetryCount"); + prop = so.FindProperty(nameof(m_RetryCount)); + ShowMixedValue(prop, otherSchemas, typeof(int), nameof(m_RetryCount)); EditorGUI.BeginChangeCheck(); int newRetryCount = (int)EditorGUILayout.IntField(prop.displayName, RetryCount); if (EditorGUI.EndChangeCheck()) @@ -682,9 +781,51 @@ public override void OnGUIMultiple(List otherSchema } EditorGUI.showMixedValue = false; + + // IncludeAddressInCatalog + prop = so.FindProperty(nameof(m_IncludeAddressInCatalog)); + ShowMixedValue(prop, otherSchemas, typeof(bool), nameof(m_IncludeAddressInCatalog)); + EditorGUI.BeginChangeCheck(); + bool newIncludeAddressInCatalog = (bool)EditorGUILayout.Toggle(prop.displayName, IncludeAddressInCatalog); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(this, "BundledAssetGroupSchemaIncludeAddressInCatalog" + name); + IncludeAddressInCatalog = newIncludeAddressInCatalog; + foreach (var schema in otherBundledSchemas) + m_QueuedChanges.Add(() => schema.IncludeAddressInCatalog = IncludeAddressInCatalog); + } + EditorGUI.showMixedValue = false; + + // IncludeGUIDInCatalog + prop = so.FindProperty(nameof(m_IncludeGUIDInCatalog)); + ShowMixedValue(prop, otherSchemas, typeof(bool), nameof(m_IncludeGUIDInCatalog)); + EditorGUI.BeginChangeCheck(); + bool newIncludeGUIDInCatalog = (bool)EditorGUILayout.Toggle(prop.displayName, IncludeGUIDInCatalog); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(this, "BundledAssetGroupSchemaIncludeGUIDInCatalog" + name); + IncludeGUIDInCatalog = newIncludeGUIDInCatalog; + foreach (var schema in otherBundledSchemas) + m_QueuedChanges.Add(() => schema.IncludeGUIDInCatalog = IncludeGUIDInCatalog); + } + EditorGUI.showMixedValue = false; + // IncludeLabelsInCatalog + prop = so.FindProperty(nameof(m_IncludeLabelsInCatalog)); + ShowMixedValue(prop, otherSchemas, typeof(bool), nameof(m_IncludeLabelsInCatalog)); + EditorGUI.BeginChangeCheck(); + bool newIncludeLabelsInCatalog = (bool)EditorGUILayout.Toggle(prop.displayName, IncludeLabelsInCatalog); + if (EditorGUI.EndChangeCheck()) + { + Undo.RecordObject(this, "BundledAssetGroupSchemaIncludeLabelsInCatalog" + name); + IncludeLabelsInCatalog = newIncludeLabelsInCatalog; + foreach (var schema in otherBundledSchemas) + m_QueuedChanges.Add(() => schema.IncludeLabelsInCatalog = IncludeLabelsInCatalog); + } + EditorGUI.showMixedValue = false; + // BundleMode - prop = so.FindProperty("m_BundleMode"); - ShowMixedValue(prop, otherSchemas, typeof(Enum), "m_BundleMode"); + prop = so.FindProperty(nameof(m_BundleMode)); + ShowMixedValue(prop, otherSchemas, typeof(Enum), nameof(m_BundleMode)); EditorGUI.BeginChangeCheck(); BundlePackingMode newBundleMode = (BundlePackingMode)EditorGUILayout.EnumPopup(prop.displayName, BundleMode); if (EditorGUI.EndChangeCheck()) @@ -696,9 +837,22 @@ public override void OnGUIMultiple(List otherSchema } EditorGUI.showMixedValue = false; + // InternalIdMode + prop = so.FindProperty(nameof(m_InternalIdNamingMode)); + ShowMixedValue(prop, otherSchemas, typeof(Enum), nameof(m_InternalIdNamingMode)); + EditorGUI.BeginChangeCheck(); + AssetNamingMode newInternalIdMode = (AssetNamingMode)EditorGUILayout.EnumPopup(prop.displayName, InternalIdNamingMode); + if (EditorGUI.EndChangeCheck()) + { + InternalIdNamingMode = newInternalIdMode; + foreach (var schema in otherBundledSchemas) + schema.InternalIdNamingMode = InternalIdNamingMode; + } + EditorGUI.showMixedValue = false; + //Bundle Naming - prop = so.FindProperty("m_BundleNaming"); - ShowMixedValue(prop, otherSchemas, typeof(Enum), "m_BundleNaming"); + prop = so.FindProperty(nameof(m_BundleNaming)); + ShowMixedValue(prop, otherSchemas, typeof(Enum), nameof(m_BundleNaming)); EditorGUI.BeginChangeCheck(); EditorGUILayout.PropertyField(prop, true); if (EditorGUI.EndChangeCheck()) @@ -712,8 +866,8 @@ public override void OnGUIMultiple(List otherSchema EditorGUI.showMixedValue = false; //Bundled Asset Provider Type - prop = so.FindProperty("m_BundledAssetProviderType"); - ShowMixedValue(prop, otherSchemas, typeof(SerializedType), "m_BundledAssetProviderType"); + prop = so.FindProperty(nameof(m_BundledAssetProviderType)); + ShowMixedValue(prop, otherSchemas, typeof(SerializedType), nameof(m_BundledAssetProviderType)); EditorGUI.BeginChangeCheck(); EditorGUILayout.PropertyField(prop, m_AssetProviderContent, true); @@ -726,8 +880,8 @@ public override void OnGUIMultiple(List otherSchema EditorGUI.showMixedValue = false; //Asset Bundle Provider Type - prop = so.FindProperty("m_AssetBundleProviderType"); - ShowMixedValue(prop, otherSchemas, typeof(SerializedType), "m_AssetBundleProviderType"); + prop = so.FindProperty(nameof(m_AssetBundleProviderType)); + ShowMixedValue(prop, otherSchemas, typeof(SerializedType), nameof(m_AssetBundleProviderType)); EditorGUI.BeginChangeCheck(); EditorGUILayout.PropertyField(prop, m_BundleProviderContent, true); diff --git a/Editor/Settings/LabelTable.cs b/Editor/Settings/LabelTable.cs index c4edcf75..25042673 100644 --- a/Editor/Settings/LabelTable.cs +++ b/Editor/Settings/LabelTable.cs @@ -16,15 +16,17 @@ class LabelTable internal List labelNames { get { return m_LabelNames; } } const int k_KNameCountCap = 3; - internal void AddLabelName(string name) + internal bool AddLabelName(string name) { - if (!m_LabelNames.Contains(name)) + if (m_LabelNames.Contains(name)) + return false; + if (name.Contains("[") && name.Contains("]")) { - if (name.Contains("[") && name.Contains("]")) - Debug.LogErrorFormat("Label name '{0}' cannot contain '[ ]'.", name); - else - m_LabelNames.Add(name); + Debug.LogErrorFormat("Label name '{0}' cannot contain '[ ]'.", name); + return false; } + m_LabelNames.Add(name); + return true; } internal string GetUniqueLabelName(string name) diff --git a/Runtime/AddressablesImpl.cs b/Runtime/AddressablesImpl.cs index a784b9f1..8a51f67c 100644 --- a/Runtime/AddressablesImpl.cs +++ b/Runtime/AddressablesImpl.cs @@ -280,6 +280,9 @@ public void ClearResourceLocators() internal bool GetResourceLocations(object key, Type type, out IList locations) { + if (type == null && (key is AssetReference)) + type = (key as AssetReference).SubOjbectType; + key = EvaluateKey(key); locations = null; diff --git a/Runtime/AssetReference.cs b/Runtime/AssetReference.cs index 2fcc2d5c..db2f29f3 100644 --- a/Runtime/AssetReference.cs +++ b/Runtime/AssetReference.cs @@ -237,6 +237,8 @@ public class AssetReference : IKeyEvaluator string m_AssetGUID = ""; [SerializeField] string m_SubObjectName; + [SerializeField] + string m_SubObjectType = null; AsyncOperationHandle m_Operation; /// @@ -275,8 +277,15 @@ public virtual object RuntimeKey /// Stores the name of the sub object. /// public virtual string SubObjectName { get { return m_SubObjectName; } set { m_SubObjectName = value; } } - - + internal virtual Type SubOjbectType + { + get + { + if (!string.IsNullOrEmpty(m_SubObjectName) && m_SubObjectType != null) + return Type.GetType(m_SubObjectType); + return null; + } + } /// /// Returns the state of the internal operation. /// @@ -329,14 +338,27 @@ public virtual Object Asset #if UNITY_EDITOR Object m_CachedAsset; + string m_CachedGUID = ""; /// /// Cached Editor Asset. /// protected Object CachedAsset { - get { return m_CachedAsset; } - set { m_CachedAsset = value; } + get + { + if (m_CachedGUID != m_AssetGUID) + { + m_CachedAsset = null; + m_CachedGUID = ""; + } + return m_CachedAsset; + } + set + { + m_CachedAsset = value; + m_CachedGUID = m_AssetGUID; + } } #endif /// @@ -346,7 +368,7 @@ protected Object CachedAsset public override string ToString() { #if UNITY_EDITOR - return "[" + m_AssetGUID + "]" + m_CachedAsset; + return "[" + m_AssetGUID + "]" + CachedAsset; #else return "[" + m_AssetGUID + "]"; #endif @@ -542,11 +564,11 @@ public virtual Object editorAsset { get { - if (m_CachedAsset != null || string.IsNullOrEmpty(m_AssetGUID)) - return m_CachedAsset; + if (CachedAsset != null || string.IsNullOrEmpty(m_AssetGUID)) + return CachedAsset; var assetPath = AssetDatabase.GUIDToAssetPath(m_AssetGUID); var mainType = AssetDatabase.GetMainAssetTypeAtPath(assetPath); - return (m_CachedAsset = AssetDatabase.LoadAssetAtPath(assetPath, mainType)); + return (CachedAsset = AssetDatabase.LoadAssetAtPath(assetPath, mainType)); } } /// @@ -561,13 +583,13 @@ public virtual bool SetEditorAsset(Object value) { if (value == null) { - m_CachedAsset = null; + CachedAsset = null; m_AssetGUID = string.Empty; m_SubObjectName = null; return true; } - if (m_CachedAsset != value) + if (CachedAsset != value) { m_SubObjectName = null; var path = AssetDatabase.GetAssetOrScenePath(value); @@ -585,7 +607,7 @@ public virtual bool SetEditorAsset(Object value) { m_AssetGUID = AssetDatabase.AssetPathToGUID(path); var mainAsset = AssetDatabase.LoadMainAssetAtPath(path); - m_CachedAsset = mainAsset; + CachedAsset = mainAsset; if (value != mainAsset) SetEditorSubObject(value); } @@ -626,9 +648,10 @@ public virtual bool SetEditorSubObject(Object value) var subAssets = AssetDatabase.LoadAllAssetRepresentationsAtPath(AssetDatabase.GUIDToAssetPath(m_AssetGUID)); foreach (var s in subAssets) { - if (s.name == value.name) + if (s.name == value.name && s.GetType() == value.GetType()) { - m_SubObjectName = value.name; + m_SubObjectName = s.name; + m_SubObjectType = s.GetType().AssemblyQualifiedName; return true; } } diff --git a/Runtime/Initialization/InitializationOperation.cs b/Runtime/Initialization/InitializationOperation.cs index de551cb2..a3d4528c 100644 --- a/Runtime/Initialization/InitializationOperation.cs +++ b/Runtime/Initialization/InitializationOperation.cs @@ -202,8 +202,25 @@ static AsyncOperationHandle OnCatalogDataLoaded(AddressablesIm public static AsyncOperationHandle LoadContentCatalog(AddressablesImpl addressables, IResourceLocation loc, string providerSuffix) { - var loadOp = addressables.LoadAssetAsync(loc); - var chainOp = addressables.ResourceManager.CreateChainOperation(loadOp, res => OnCatalogDataLoaded(addressables, res, providerSuffix)); + Type provType = typeof(ProviderOperation); + var catalogOp = addressables.ResourceManager.CreateOperation>( provType, provType.GetHashCode(), 0, null ); + + IResourceProvider catalogProvider = null; + foreach( IResourceProvider provider in addressables.ResourceManager.ResourceProviders ) + { + if( provider is ContentCatalogProvider ) + { + catalogProvider = provider; + break; + } + } + + var dependencies = addressables.ResourceManager.CreateGroupOperation( loc.Dependencies, true ); + catalogOp.Init( addressables.ResourceManager, catalogProvider, loc, dependencies, true ); + var catalogHandle = addressables.ResourceManager.StartOperation( catalogOp, dependencies ); + dependencies.Release(); + + var chainOp = addressables.ResourceManager.CreateChainOperation(catalogHandle, res => OnCatalogDataLoaded(addressables, res, providerSuffix)); return chainOp; } diff --git a/Runtime/ResourceLocators/ContentCatalogData.cs b/Runtime/ResourceLocators/ContentCatalogData.cs index f4fd14ad..8bd26a30 100644 --- a/Runtime/ResourceLocators/ContentCatalogData.cs +++ b/Runtime/ResourceLocators/ContentCatalogData.cs @@ -59,16 +59,6 @@ public ContentCatalogDataEntry(Type type, string internalId, string provider, IE Dependencies = dependencies == null ? new List() : new List(dependencies); Data = extraData; } - - internal int ComputeDependencyHash() - { - if (Dependencies == null) - return 0; - int hash = 0; - foreach (var d in Dependencies) - hash = hash * 31 + d.GetHashCode(); - return hash; - } } /// @@ -168,7 +158,7 @@ public string[] InternalIds [SerializeField] internal string m_EntryDataString = null; - const int k_ByteSize = 4; + const int kBytesPerInt32 = 4; const int k_EntryDataItemPerEntry = 7; [FormerlySerializedAs("m_extraDataString")] @@ -176,10 +166,10 @@ public string[] InternalIds internal string m_ExtraDataString = null; [SerializeField] - internal string[] m_Keys = null; + internal SerializedType[] m_resourceTypes = null; [SerializeField] - internal SerializedType[] m_resourceTypes = null; + string[] m_InternalIdPrefixes = null; struct Bucket { @@ -198,6 +188,7 @@ class CompactLocation : IResourceLocation int m_DependencyHashCode; string m_PrimaryKey; Type m_Type; + public string InternalId { get { return m_InternalId; } } public string ProviderId { get { return m_ProviderId; } } public IList Dependencies @@ -256,7 +247,6 @@ internal void CleanData() m_EntryDataString = ""; m_ExtraDataString = ""; m_InternalIds = null; - m_Keys = null; m_LocatorId = ""; m_ProviderIds = null; m_ResourceProviderData = null; @@ -298,7 +288,7 @@ public ResourceLocationMap CreateLocator(string providerSuffix = null) { for (int i = 0; i < m_ProviderIds.Length; i++) { - if (!m_ProviderIds[i].EndsWith(providerSuffix)) + if (!m_ProviderIds[i].EndsWith(providerSuffix, StringComparison.Ordinal)) m_ProviderIds[i] = m_ProviderIds[i] + providerSuffix; } } @@ -314,40 +304,55 @@ public ResourceLocationMap CreateLocator(string providerSuffix = null) var entryData = Convert.FromBase64String(m_EntryDataString); int count = SerializationUtilities.ReadInt32FromByteArray(entryData, 0); - List locations = new List(count); + var locations = new IResourceLocation[count]; for (int i = 0; i < count; i++) { - var index = k_ByteSize + i * k_ByteSize * k_EntryDataItemPerEntry; + var index = kBytesPerInt32 + i * (kBytesPerInt32 * k_EntryDataItemPerEntry); var internalId = SerializationUtilities.ReadInt32FromByteArray(entryData, index); - index += k_ByteSize; + index += kBytesPerInt32; var providerIndex = SerializationUtilities.ReadInt32FromByteArray(entryData, index); - index += k_ByteSize; - var dependency = SerializationUtilities.ReadInt32FromByteArray(entryData, index); - index += k_ByteSize; + index += kBytesPerInt32; + var dependencyKeyIndex = SerializationUtilities.ReadInt32FromByteArray(entryData, index); + index += kBytesPerInt32; var depHash = SerializationUtilities.ReadInt32FromByteArray(entryData, index); - index += k_ByteSize; + index += kBytesPerInt32; var dataIndex = SerializationUtilities.ReadInt32FromByteArray(entryData, index); - index += k_ByteSize; + index += kBytesPerInt32; var primaryKey = SerializationUtilities.ReadInt32FromByteArray(entryData, index); - index += k_ByteSize; + index += kBytesPerInt32; var resourceType = SerializationUtilities.ReadInt32FromByteArray(entryData, index); object data = dataIndex < 0 ? null : SerializationUtilities.ReadObjectFromByteArray(extraData, dataIndex); - locations.Add(new CompactLocation(locator, Addressables.ResolveInternalId(m_InternalIds[internalId]), - m_ProviderIds[providerIndex], dependency < 0 ? null : keys[dependency], data, depHash, m_Keys[primaryKey], m_resourceTypes[resourceType].Value)); + locations[i] = new CompactLocation(locator, Addressables.ResolveInternalId(ExpandInternalId(m_InternalIdPrefixes, m_InternalIds[internalId])), + m_ProviderIds[providerIndex], dependencyKeyIndex < 0 ? null : keys[dependencyKeyIndex], data, depHash, keys[primaryKey].ToString(), m_resourceTypes[resourceType].Value); } for (int i = 0; i < buckets.Length; i++) { var bucket = buckets[i]; var key = keys[i]; - var locs = new List(bucket.entries.Length); - foreach (var index in bucket.entries) - locs.Add(locations[index]); + var locs = new IResourceLocation[bucket.entries.Length]; + for (int b = 0; b < bucket.entries.Length; b++) + locs[b] = locations[bucket.entries[b]]; locator.Add(key, locs); } + return locator; } - + + internal static string ExpandInternalId(string[] internalIdPrefixes, string v) + { + if (internalIdPrefixes == null || internalIdPrefixes.Length == 0) + return v; + int nextHash = v.LastIndexOf('#'); + if (nextHash < 0) + return v; + int index = 0; + var numStr = v.Substring(0, nextHash); + if (!int.TryParse(numStr, out index)) + return v; + return internalIdPrefixes[index] + v.Substring(nextHash + 1); + } + /// /// Create a new ContentCatalogData object without any data. /// @@ -364,7 +369,12 @@ public ContentCatalogData() public ContentCatalogData(IList entries, string id = null) { m_LocatorId = id; - SetData(entries); + SetData(entries, false); + } + + internal ContentCatalogData(string id) + { + m_LocatorId = id; } class KeyIndexer @@ -435,6 +445,11 @@ public void Add(TKey key, TVal val) /// /// The list of public void SetData(IList data) + { + SetData(data, false); + } + + internal void SetData(IList data, bool optimizeSize) { if (data == null) return; @@ -446,7 +461,7 @@ public void SetData(IList data) keys.Add(data.SelectMany(s => s.Dependencies)); var keyIndexToEntries = new KeyIndexer, object>(keys.values, s => new List(), keys.values.Count); var entryToIndex = new Dictionary(data.Count); - var extraDataList = new List(); + var extraDataList = new List(8*1024); var entryIndexToExtraDataIndex = new Dictionary(); int extraDataIndex = 0; @@ -502,14 +517,19 @@ public void SetData(IList data) m_InternalIds = internalIds.values.ToArray(); m_ProviderIds = providers.values.ToArray(); m_resourceTypes = types.values.Select(t => new SerializedType() { Value = t }).ToArray(); - m_Keys = keys.values.Where(x => x != null) - .Select(x => x.ToString()) - .ToArray(); + if (optimizeSize) + { + var internalIdPrefixes = new List(); + var prefixIndices = new Dictionary(); + for (int i = 0; i < m_InternalIds.Length; i++) + m_InternalIds[i] = ExtractCommonPrefix(internalIdPrefixes, prefixIndices, m_InternalIds[i]); + m_InternalIdPrefixes = internalIdPrefixes.ToArray(); + } //serialize entries { - var entryData = new byte[data.Count * k_ByteSize * k_EntryDataItemPerEntry + k_ByteSize]; + var entryData = new byte[data.Count * (kBytesPerInt32 * k_EntryDataItemPerEntry) + kBytesPerInt32]; var entryDataOffset = SerializationUtilities.WriteInt32ToByteArray(entryData, data.Count, 0); for (int i = 0; i < data.Count; i++) { @@ -517,10 +537,10 @@ public void SetData(IList data) entryDataOffset = SerializationUtilities.WriteInt32ToByteArray(entryData, internalIds.map[e.InternalId], entryDataOffset); entryDataOffset = SerializationUtilities.WriteInt32ToByteArray(entryData, providers.map[e.Provider], entryDataOffset); entryDataOffset = SerializationUtilities.WriteInt32ToByteArray(entryData, e.Dependencies.Count == 0 ? -1 : keyIndexToEntries.map[e.Dependencies[0]], entryDataOffset); - entryDataOffset = SerializationUtilities.WriteInt32ToByteArray(entryData, e.ComputeDependencyHash(), entryDataOffset); + entryDataOffset = SerializationUtilities.WriteInt32ToByteArray(entryData, GetHashCodeForEnumerable(e.Dependencies), entryDataOffset); entryDataOffset = SerializationUtilities.WriteInt32ToByteArray(entryData, entryIndexToExtraDataIndex[i], entryDataOffset); entryDataOffset = SerializationUtilities.WriteInt32ToByteArray(entryData, keys.map[e.Keys.First()], entryDataOffset); - entryDataOffset = SerializationUtilities.WriteInt32ToByteArray(entryData, types.map[e.ResourceType], entryDataOffset); + entryDataOffset = SerializationUtilities.WriteInt32ToByteArray(entryData, (ushort)types.map[e.ResourceType], entryDataOffset); } m_EntryDataString = Convert.ToBase64String(entryData); } @@ -529,7 +549,7 @@ public void SetData(IList data) { var entryCount = keyIndexToEntries.values.Aggregate(0, (a, s) => a += s.Count); var bucketData = new byte[4 + keys.values.Count * 8 + entryCount * 4]; - var keyData = new List(keys.values.Count * 10); + var keyData = new List(keys.values.Count * 100); keyData.AddRange(BitConverter.GetBytes(keys.values.Count)); int keyDataOffset = 4; int bucketDataOffset = SerializationUtilities.WriteInt32ToByteArray(bucketData, keys.values.Count, 0); @@ -548,6 +568,21 @@ public void SetData(IList data) } } + internal static string ExtractCommonPrefix(List internalIdPrefixes, Dictionary prefixIndices, string v) + { + var s = v.LastIndexOf('/'); + if (s <= 0) + return v; + var prefix = v.Substring(0, s); + int index; + if (!prefixIndices.TryGetValue(prefix, out index)) + { + prefixIndices.Add(prefix, index = internalIdPrefixes.Count); + internalIdPrefixes.Add(prefix); + } + return string.Format("{0}#{1}", index, v.Substring(s)); + } + internal int CalculateCollectedHash(List objects, Dictionary hashSources) { var hashSource = new HashSet(objects); @@ -570,226 +605,6 @@ internal static int GetHashCodeForEnumerable(IEnumerable set) hash = hash * 31 + o.GetHashCode(); return hash; } - -#if REFERENCE_IMPLEMENTATION - public void SetDataOld(List locations, List labels) - { - var tmpEntries = new List(locations.Count); - var providers = new List(10); - var providerIndices = new Dictionary(10); - var countEstimate = locations.Count * 2 + labels.Count; - var internalIdToEntryIndex = new Dictionary(countEstimate); - var internalIdList = new List(countEstimate); - List keys = new List(countEstimate); - - var keyToIndex = new Dictionary(countEstimate); - var tmpBuckets = new Dictionary>(countEstimate); - - for (int i = 0; i < locations.Count; i++) - { - var rld = locations[i]; - int providerIndex = 0; - if (!providerIndices.TryGetValue(rld.m_provider, out providerIndex)) - { - providerIndices.Add(rld.m_provider, providerIndex = providers.Count); - providers.Add(rld.m_provider); - } - - int internalIdIndex = 0; - if (!internalIdToEntryIndex.TryGetValue(rld.m_internalId, out internalIdIndex)) - { - internalIdToEntryIndex.Add(rld.m_internalId, internalIdIndex = internalIdList.Count); - internalIdList.Add(rld.m_internalId); - } - - var e = new Entry() { internalId = internalIdIndex, providerIndex = (byte)providerIndex, dependency = -1 }; - if (rld.m_type == ResourceLocationData.LocationType.Int) - AddToBucket(tmpBuckets, keyToIndex, keys, int.Parse(rld.m_address), tmpEntries.Count, 1); - else if (rld.m_type == ResourceLocationData.LocationType.String) - AddToBucket(tmpBuckets, keyToIndex, keys, rld.m_address, tmpEntries.Count, 1); - if (!string.IsNullOrEmpty(rld.m_guid)) - AddToBucket(tmpBuckets, keyToIndex, keys, Hash128.Parse(rld.m_guid), tmpEntries.Count, 1); - if (rld.m_labelMask != 0) - { - for (int t = 0; t < labels.Count; t++) - { - if ((rld.m_labelMask & (1 << t)) != 0) - AddToBucket(tmpBuckets, keyToIndex, keys, labels[t], tmpEntries.Count, 100); - } - } - - tmpEntries.Add(e); - } - - for (int i = 0; i < locations.Count; i++) - { - var rld = locations[i]; - int dependency = -1; - if (rld.m_dependencies != null && rld.m_dependencies.Length > 0) - { - if (rld.m_dependencies.Length == 1) - { - dependency = keyToIndex[rld.m_dependencies[0]]; - } - else - { - System.Text.StringBuilder sb = new System.Text.StringBuilder(); - foreach (var d in rld.m_dependencies) - sb.Append(d); - var key = sb.ToString().GetHashCode(); - int keyIndex = -1; - foreach (var d in rld.m_dependencies) - { - var ki = keyToIndex[d]; - var depBucket = tmpBuckets[ki]; - keyIndex = AddToBucket(tmpBuckets, keyToIndex, keys, key, depBucket[0], 10); - } - dependency = keyIndex; - } - var e = tmpEntries[i]; - e.dependency = dependency; - tmpEntries[i] = e; - } - } - - m_internalIds = internalIdList.ToArray(); - m_providerIds = providers.ToArray(); - var entryData = new byte[tmpEntries.Count * 4 * 3 + 4]; - var offset = Serialize(entryData, tmpEntries.Count, 0); - for (int i = 0; i < tmpEntries.Count; i++) - { - var e = tmpEntries[i]; - offset = Serialize(entryData, e.internalId, offset); - offset = Serialize(entryData, e.providerIndex, offset); - offset = Serialize(entryData, e.dependency, offset); - } - m_entryDataString = Convert.ToBase64String(entryData); - - int bucketEntryCount = 0; - var bucketList = new List(keys.Count); - for (int i = 0; i < keys.Count; i++) - { - var bucketIndex = keyToIndex[keys[i]]; - List entries = tmpBuckets[bucketIndex]; - bucketList.Add(new Bucket() { entries = entries.ToArray() }); - bucketEntryCount += entries.Count; - } - - var keyData = new List(bucketList.Count * 10); - keyData.AddRange(BitConverter.GetBytes(bucketList.Count)); - int dataOffset = 4; - for (int i = 0; i < bucketList.Count; i++) - { - var bucket = bucketList[i]; - bucket.dataOffset = dataOffset; - bucketList[i] = bucket; - var key = keys[i]; - var kt = key.GetType(); - if (kt == typeof(string)) - { - string str = key as string; - byte[] tmp = System.Text.Encoding.Unicode.GetBytes(str); - byte[] tmp2 = System.Text.Encoding.ASCII.GetBytes(str); - if (System.Text.Encoding.Unicode.GetString(tmp) == System.Text.Encoding.ASCII.GetString(tmp2)) - { - keyData.Add((byte)KeyType.ASCIIString); - keyData.AddRange(tmp2); - dataOffset += tmp2.Length + 1; - } - else - { - keyData.Add((byte)KeyType.UnicodeString); - keyData.AddRange(tmp); - dataOffset += tmp.Length + 1; - } - } - else if (kt == typeof(UInt32)) - { - byte[] tmp = BitConverter.GetBytes((UInt32)key); - keyData.Add((byte)KeyType.UInt32); - keyData.AddRange(tmp); - dataOffset += tmp.Length + 1; - } - else if (kt == typeof(UInt16)) - { - byte[] tmp = BitConverter.GetBytes((UInt16)key); - keyData.Add((byte)KeyType.UInt16); - keyData.AddRange(tmp); - dataOffset += tmp.Length + 1; - } - else if (kt == typeof(Int32)) - { - byte[] tmp = BitConverter.GetBytes((Int32)key); - keyData.Add((byte)KeyType.Int32); - keyData.AddRange(tmp); - dataOffset += tmp.Length + 1; - } - else if (kt == typeof(int)) - { - byte[] tmp = BitConverter.GetBytes((UInt32)key); - keyData.Add((byte)KeyType.UInt32); - keyData.AddRange(tmp); - dataOffset += tmp.Length + 1; - } - else if (kt == typeof(Hash128)) - { - var guid = (Hash128)key; - byte[] tmp = System.Text.Encoding.ASCII.GetBytes(guid.ToString()); - keyData.Add((byte)KeyType.Hash128); - keyData.AddRange(tmp); - dataOffset += tmp.Length + 1; - } - } - m_keyDataString = Convert.ToBase64String(keyData.ToArray()); - - var bucketData = new byte[4 + bucketList.Count * 8 + bucketEntryCount * 4]; - offset = Serialize(bucketData, bucketList.Count, 0); - for (int i = 0; i < bucketList.Count; i++) - { - offset = Serialize(bucketData, bucketList[i].dataOffset, offset); - offset = Serialize(bucketData, bucketList[i].entries.Length, offset); - foreach (var e in bucketList[i].entries) - offset = Serialize(bucketData, e, offset); - } - m_bucketDataString = Convert.ToBase64String(bucketData); - -#if SERIALIZE_CATALOG_AS_BINARY - //TODO: investigate saving catalog as binary - roughly 20% size decrease, still needs a provider implementation - var stream = new System.IO.MemoryStream(); - var bw = new System.IO.BinaryWriter(stream); - foreach (var i in m_internalIds) - bw.Write(i); - foreach (var p in m_providerIds) - bw.Write(p); - bw.Write(entryData); - bw.Write(keyData.ToArray()); - bw.Write(bucketData); - bw.Flush(); - bw.Close(); - stream.Flush(); - System.IO.File.WriteAllBytes("Library/catalog_binary.bytes", stream.ToArray()); - System.IO.File.WriteAllText("Library/catalog_binary.txt", Convert.ToBase64String(stream.ToArray())); - stream.Close(); -#endif - } - - private int AddToBucket(Dictionary> buckets, Dictionary keyToIndex, List keys, object key, int index, int sizeHint) - { - int keyIndex = -1; - if (!keyToIndex.TryGetValue(key, out keyIndex)) - { - keyToIndex.Add(key, keyIndex = keys.Count); - keys.Add(key); - } - - List bucket; - if (!buckets.TryGetValue(keyIndex, out bucket)) - buckets.Add(keyIndex, bucket = new List(sizeHint)); - bucket.Add(index); - return keyIndex; - } - -#endif #endif } } diff --git a/Runtime/ResourceLocators/DynamicResourceLocators.cs b/Runtime/ResourceLocators/DynamicResourceLocators.cs index bb922485..5bf35155 100644 --- a/Runtime/ResourceLocators/DynamicResourceLocators.cs +++ b/Runtime/ResourceLocators/DynamicResourceLocators.cs @@ -44,7 +44,7 @@ public bool Locate(object key, Type type, out IList locations return false; } - void CreateDynamicLocations(Type type, IList locations, string locName, string subKey, IResourceLocation mainLoc) + internal void CreateDynamicLocations(Type type, IList locations, string locName, string subKey, IResourceLocation mainLoc) { if (type == typeof(Sprite) && mainLoc.ResourceType == typeof(U2D.SpriteAtlas)) { @@ -53,9 +53,9 @@ void CreateDynamicLocations(Type type, IList locations, strin else { if (mainLoc.HasDependencies) - locations.Add(new ResourceLocationBase(locName, $"{mainLoc.InternalId}[{subKey}]", mainLoc.ProviderId, type, mainLoc.Dependencies.ToArray())); + locations.Add(new ResourceLocationBase(locName, $"{mainLoc.InternalId}[{subKey}]", mainLoc.ProviderId, mainLoc.ResourceType, mainLoc.Dependencies.ToArray())); else - locations.Add(new ResourceLocationBase(locName, $"{mainLoc.InternalId}[{subKey}]", mainLoc.ProviderId, type)); + locations.Add(new ResourceLocationBase(locName, $"{mainLoc.InternalId}[{subKey}]", mainLoc.ProviderId, mainLoc.ResourceType)); } } } diff --git a/Runtime/ResourceLocators/ResourceLocationMap.cs b/Runtime/ResourceLocators/ResourceLocationMap.cs index 53bd5809..8443ab4e 100644 --- a/Runtime/ResourceLocators/ResourceLocationMap.cs +++ b/Runtime/ResourceLocators/ResourceLocationMap.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using UnityEngine.ResourceManagement; using UnityEngine.ResourceManagement.ResourceLocations; diff --git a/Runtime/ResourceManager/AsyncOperations/AsyncOperationBase.cs b/Runtime/ResourceManager/AsyncOperations/AsyncOperationBase.cs index 2bc9f807..85907665 100644 --- a/Runtime/ResourceManager/AsyncOperations/AsyncOperationBase.cs +++ b/Runtime/ResourceManager/AsyncOperations/AsyncOperationBase.cs @@ -93,14 +93,12 @@ protected virtual void GetDependencies(List dependencies) internal int m_Version; internal int Version { get { return m_Version; } } - DelegateList m_CompletedAction; DelegateList m_DestroyedAction; DelegateList> m_CompletedActionT; internal bool CompletedEventHasListeners => m_CompletedActionT != null && m_CompletedActionT.Count > 0; internal bool DestroyedEventHasListeners => m_DestroyedAction != null && m_DestroyedAction.Count > 0; - internal bool CompletedTypelessEventHasListeners => m_CompletedAction != null && m_CompletedAction.Count > 0; - + Action m_OnDestroyAction; internal Action OnDestroy { set { m_OnDestroyAction = value; } } internal int ReferenceCount { get { return m_referenceCount; } } @@ -308,14 +306,11 @@ internal event Action CompletedTypeless { add { - if (m_CompletedAction == null) - m_CompletedAction = DelegateList.CreateWithGlobalCache(); - m_CompletedAction.Add(value); - RegisterForDeferredCallbackEvent(); + Completed += s => value(s); } remove { - m_CompletedAction?.Remove(value); + Completed -= s => value(s); } } @@ -357,17 +352,12 @@ internal float PercentComplete internal void InvokeCompletionEvent() { - if (m_CompletedAction != null) - { - m_CompletedAction.Invoke(new AsyncOperationHandle(this)); - m_CompletedAction.Clear(); - } - if (m_CompletedActionT != null) { m_CompletedActionT.Invoke(new AsyncOperationHandle(this)); m_CompletedActionT.Clear(); } + if (m_waitHandle != null) m_waitHandle.Set(); diff --git a/Runtime/ResourceManager/AsyncOperations/GroupOperation.cs b/Runtime/ResourceManager/AsyncOperations/GroupOperation.cs index 96f3c694..fc024f9e 100644 --- a/Runtime/ResourceManager/AsyncOperations/GroupOperation.cs +++ b/Runtime/ResourceManager/AsyncOperations/GroupOperation.cs @@ -9,9 +9,17 @@ namespace UnityEngine.ResourceManagement.AsyncOperations { class GroupOperation : AsyncOperationBase>, ICachable { + [Flags] + public enum GroupOperationSettings + { + None = 0, + ReleaseDependenciesOnFailure = 1, + AllowFailedDependencies = 2 + } + Action m_InternalOnComplete; int m_LoadedCount; - bool m_ReleaseDependenciesOnFailure = true; + GroupOperationSettings m_Settings; string debugName = null; private const int k_MaxDisplayedLocationLength = 45; @@ -135,16 +143,19 @@ private void CompleteIfDependenciesComplete() { bool success = true; string errorMsg = string.Empty; - for (int i = 0; i < Result.Count; i++) + if (!m_Settings.HasFlag(GroupOperationSettings.AllowFailedDependencies)) { - if (Result[i].Status != AsyncOperationStatus.Succeeded) + for (int i = 0; i < Result.Count; i++) { - success = false; - errorMsg = Result[i].OperationException != null ? Result[i].OperationException.Message : string.Empty; - break; + if (Result[i].Status != AsyncOperationStatus.Succeeded) + { + success = false; + errorMsg = Result[i].OperationException != null ? Result[i].OperationException.Message : string.Empty; + break; + } } } - Complete(Result, success, errorMsg, m_ReleaseDependenciesOnFailure); + Complete(Result, success, errorMsg, m_Settings.HasFlag(GroupOperationSettings.ReleaseDependenciesOnFailure)); } } @@ -172,10 +183,18 @@ protected override float Progress } - public void Init(List operations, bool releaseDependenciesOnFailure = true) + public void Init(List operations, bool releaseDependenciesOnFailure = true, bool allowFailedDependencies = false) + { + Result = new List(operations); + m_Settings = releaseDependenciesOnFailure ? GroupOperationSettings.ReleaseDependenciesOnFailure : GroupOperationSettings.None; + if( allowFailedDependencies ) + m_Settings |= GroupOperationSettings.AllowFailedDependencies; + } + + public void Init(List operations, GroupOperationSettings settings) { Result = new List(operations); - m_ReleaseDependenciesOnFailure = releaseDependenciesOnFailure; + m_Settings = settings; } void OnOperationCompleted(AsyncOperationHandle op) diff --git a/Runtime/ResourceManager/ResourceLocations/ResourceLocationBase.cs b/Runtime/ResourceManager/ResourceLocations/ResourceLocationBase.cs index 5b1a6bcd..4415f615 100644 --- a/Runtime/ResourceManager/ResourceLocations/ResourceLocationBase.cs +++ b/Runtime/ResourceManager/ResourceLocations/ResourceLocationBase.cs @@ -95,7 +95,7 @@ public ResourceLocationBase(string name, string id, string providerId, Type t, p m_Id = id; m_ProviderId = providerId; m_Dependencies = new List(dependencies); - m_Type = t; + m_Type = t == null ? typeof(object) : t; ComputeDependencyHash(); } diff --git a/Runtime/ResourceManager/ResourceManager.cs b/Runtime/ResourceManager/ResourceManager.cs index 26e16fc0..3e10fc3f 100644 --- a/Runtime/ResourceManager/ResourceManager.cs +++ b/Runtime/ResourceManager/ResourceManager.cs @@ -600,6 +600,27 @@ public AsyncOperationHandle> CreateGroupOperation op.Init(ops); return StartOperation(op, default); } + + /// + /// Create a group operation for a set of locations. + /// + /// The expected object type for the operations. + /// The list of locations to load. + /// The operation succeeds if any grouped locations fail. + /// The operation for the entire group. + internal AsyncOperationHandle> CreateGroupOperation(IList locations, bool allowFailedDependencies) + { + var op = CreateOperation(typeof(GroupOperation), s_GroupOperationTypeHash, 0, m_ReleaseOpNonCached); + var ops = new List(locations.Count); + foreach (var loc in locations) + ops.Add(ProvideResource(loc)); + + GroupOperation.GroupOperationSettings settings = GroupOperation.GroupOperationSettings.None; + if( allowFailedDependencies ) + settings |= GroupOperation.GroupOperationSettings.AllowFailedDependencies; + op.Init(ops, settings); + return StartOperation(op, default); + } /// /// Create a group operation for a set of AsyncOperationHandles diff --git a/Runtime/ResourceManager/ResourceProviders/AssetBundleProvider.cs b/Runtime/ResourceManager/ResourceProviders/AssetBundleProvider.cs index 0d9b9872..6fdcb3ef 100644 --- a/Runtime/ResourceManager/ResourceProviders/AssetBundleProvider.cs +++ b/Runtime/ResourceManager/ResourceProviders/AssetBundleProvider.cs @@ -276,7 +276,7 @@ private void WebRequestOperationCompleted(AsyncOperation op) #if ENABLE_CACHING if (!string.IsNullOrEmpty(m_Options.Hash)) { - CachedAssetBundle cab = new CachedAssetBundle(m_Options.BundleName, Hash128.Compute(m_Options.Hash)); + CachedAssetBundle cab = new CachedAssetBundle(m_Options.BundleName, Hash128.Parse(m_Options.Hash)); if (Caching.IsVersionCached(cab)) { message = string.Format("Web request {0} failed to load from cache with error '{1}'. The cached AssetBundle will be cleared from the cache and re-downloaded. Retrying...", webReq.url, webReq.error); diff --git a/Runtime/Utility/SerializationUtilities.cs b/Runtime/Utility/SerializationUtilities.cs index 0bf4b53d..94bdd81d 100644 --- a/Runtime/Utility/SerializationUtilities.cs +++ b/Runtime/Utility/SerializationUtilities.cs @@ -6,7 +6,7 @@ namespace UnityEngine.AddressableAssets.Utility { - static class SerializationUtilities + internal static class SerializationUtilities { internal enum ObjectType { @@ -33,7 +33,18 @@ internal static int WriteInt32ToByteArray(byte[] data, int val, int offset) data[offset + 3] = (byte)((val >> 24) & 0xFF); return offset + 4; } + /* + internal static ushort ReadUInt16FromByteArray(byte[] data, int offset) + { + return (ushort)(data[offset] | (data[offset + 1] << 8)); + } + internal static int WriteUInt16ToByteArray(byte[] data, ushort val, int offset) + { + data[offset] = (byte)(val & 0xFF); + data[offset + 1] = (byte)((val >> 8) & 0xFF); + return offset + 2; + }*/ /// /// Deserializes an object from an array at a specified index. Supported types are ASCIIString, UnicodeString, UInt16, UInt32, Int32, Hash128, JsonObject /// diff --git a/Tests/Editor/AddressableAssetEntryTests.cs b/Tests/Editor/AddressableAssetEntryTests.cs index 15e682a5..45a45ed4 100644 --- a/Tests/Editor/AddressableAssetEntryTests.cs +++ b/Tests/Editor/AddressableAssetEntryTests.cs @@ -45,6 +45,40 @@ protected override void OnCleanup() Settings.RemoveGroup(m_testGroup); } + [Test] + public void CreateKeyList_Returns_ExpectedKeys() + { + var e = Settings.DefaultGroup.GetAssetEntry(m_guid); + e.SetAddress("address"); + e.SetLabel("label", true, true, true); + CollectionAssert.AreEqual(new string[] { "address", m_guid, "label" }, e.CreateKeyList(true, true, true)); + CollectionAssert.AreEqual(new string[] { m_guid, "label" }, e.CreateKeyList(false, true, true)); + CollectionAssert.AreEqual(new string[] { "address", "label" }, e.CreateKeyList(true, false, true)); + CollectionAssert.AreEqual(new string[] { "address", "label" }, e.CreateKeyList(true, false, true)); + CollectionAssert.AreEqual(new string[] {"label" }, e.CreateKeyList(false, false, true)); + CollectionAssert.AreEqual(new string[] { "address" }, e.CreateKeyList(true, false, false)); + CollectionAssert.AreEqual(new string[] { m_guid }, e.CreateKeyList(false, true, false)); + } + + [Test] + public void GetAssetLoadPath_Returns_ExpectedPath() + { + var schema = Settings.DefaultGroup.GetSchema(); + var e = Settings.DefaultGroup.GetAssetEntry(m_guid); + schema.InternalIdNamingMode = BundledAssetGroupSchema.AssetNamingMode.FullPath; + Assert.AreEqual(e.AssetPath, e.GetAssetLoadPath(true, null)); + schema.InternalIdNamingMode = BundledAssetGroupSchema.AssetNamingMode.Filename; + Assert.AreEqual(Path.GetFileName(e.AssetPath), e.GetAssetLoadPath(true, null)); + schema.InternalIdNamingMode = BundledAssetGroupSchema.AssetNamingMode.GUID; + Assert.AreEqual(m_guid, e.GetAssetLoadPath(true, null)); + schema.InternalIdNamingMode = BundledAssetGroupSchema.AssetNamingMode.Dynamic; + Assert.AreEqual(m_guid, e.GetAssetLoadPath(true, null)); + Assert.AreEqual(m_guid.Substring(0, 1), e.GetAssetLoadPath(true, new HashSet())); + var hs = new HashSet(); + hs.Add(m_guid.Substring(0, 1)); + Assert.AreEqual(m_guid.Substring(0, 2), e.GetAssetLoadPath(true, hs)); + } + [Test] public void CreateCatalogEntries_WhenObjectHasMultipleSubObjectWithSameType_OnlyOneSubEntryIsCreated() { @@ -486,6 +520,37 @@ public void GatherAllAssetReferenceDrawableEntries_AddsAllEntries_FromAddressabl Settings.RemoveAssetEntry(guid, false); Settings.RemoveAssetEntry(collectionGuid, false); } + + [Test] + public void GatherResourcesEntries_GathersAllResourceEntries_IncludingLowercase() + { + var resourcePath = GetAssetPath("Resources"); + string testAssetFolder = GetAssetPath("TestFolder"); + var subFolderPath = testAssetFolder + "/resources"; + Directory.CreateDirectory(subFolderPath); + if(!Directory.Exists(resourcePath)) + Directory.CreateDirectory(resourcePath); + + var r1GUID = CreateAsset(resourcePath + "/testResourceupper.prefab", "testResourceupper"); + var r2GUID = CreateAsset(subFolderPath + "/testResourcelower.prefab", "testResourcelower"); + + var group = Settings.FindGroup(AddressableAssetSettings.PlayerDataGroupName); + var resourceEntry = Settings.CreateOrMoveEntry(AddressableAssetEntry.ResourcesName, group, false); + + var entries = new List(); + resourceEntry.GatherResourcesEntries(entries, true, null); + + // Assert + Assert.IsTrue(entries.Any(e => e.guid == r1GUID)); + Assert.IsTrue(entries.Any(e => e.guid == r2GUID)); + + // Cleanup + Directory.Delete(resourcePath, true); + Directory.Delete(subFolderPath, true); + Settings.RemoveAssetEntry(r1GUID); + Settings.RemoveAssetEntry(r2GUID); + Settings.RemoveAssetEntry(AddressableAssetEntry.ResourcesName); + } [Test] public void GetRuntimeProviderType_HandlesEmptyProviderString() diff --git a/Tests/Editor/AddressableAssetSettingsTests.cs b/Tests/Editor/AddressableAssetSettingsTests.cs index 86ec1ea0..bd455ef3 100644 --- a/Tests/Editor/AddressableAssetSettingsTests.cs +++ b/Tests/Editor/AddressableAssetSettingsTests.cs @@ -959,7 +959,7 @@ public void AddressableAssetSettings_SetLabelValueForEntries_CanSet(int numEntri Settings.SetLabelValueForEntries(entries, newLabel, true, true); foreach(var e in entries) Assert.IsTrue(e.labels.Contains(newLabel)); - Assert.AreEqual(prevDC + 2, EditorUtility.GetDirtyCount(Settings)); + Assert.AreEqual(prevDC + 1, EditorUtility.GetDirtyCount(Settings)); // Cleanup Settings.RemoveLabel(newLabel); diff --git a/Tests/Editor/AddressableAssetUtilityTests.cs b/Tests/Editor/AddressableAssetUtilityTests.cs index a0d9096b..bb380d8c 100644 --- a/Tests/Editor/AddressableAssetUtilityTests.cs +++ b/Tests/Editor/AddressableAssetUtilityTests.cs @@ -4,6 +4,7 @@ using System.Reflection; using NUnit.Framework; using UnityEditor.AddressableAssets.Settings; +using UnityEditor.AddressableAssets.Settings.GroupSchemas; using UnityEditor.Build.Utilities; using UnityEngine; using UnityEngine.Audio; @@ -273,5 +274,23 @@ public void SafeMoveResourcesToGroup_WithInvalidParameters_Fails() Assert.IsFalse(AddressableAssetUtility.SafeMoveResourcesToGroup(Settings, null, null, null, false)); Assert.IsFalse(AddressableAssetUtility.SafeMoveResourcesToGroup(Settings, Settings.DefaultGroup, null, null, false)); } + + + HashSet otherInternaIds = new HashSet(new string[] { "a", "ab", "abc" }); + + [TestCase(BundledAssetGroupSchema.AssetNamingMode.FullPath, "Assets/blah/something.asset", "", "Assets/blah/something.asset")] + [TestCase(BundledAssetGroupSchema.AssetNamingMode.Filename, "Assets/blah/something.asset", "", "something.asset")] + [TestCase(BundledAssetGroupSchema.AssetNamingMode.GUID, "Assets/blah/something.asset", "guidstring", "guidstring")] + [TestCase(BundledAssetGroupSchema.AssetNamingMode.Dynamic, "Assets/blah/something.asset", "guidstring", "g")] + [TestCase(BundledAssetGroupSchema.AssetNamingMode.Dynamic, "Assets/blah/something.asset", "abcd_guidstring", "abcd")] + [Test] + public void BundledAssetGroupSchema_GetAssetLoadPath_Returns_ExpectedId(int imode, string assetPath, string guid, string expectedId) + { + var mode = (BundledAssetGroupSchema.AssetNamingMode)imode; + var bas = Settings.DefaultGroup.GetSchema(); + bas.InternalIdNamingMode = mode; + var actualId = bas.GetAssetLoadPath(assetPath, otherInternaIds, s => guid); + Assert.AreEqual(expectedId, actualId); + } } } diff --git a/Tests/Editor/AssetReferenceDrawerTests.cs b/Tests/Editor/AssetReferenceDrawerTests.cs index 77df3ff6..5e79f7dd 100644 --- a/Tests/Editor/AssetReferenceDrawerTests.cs +++ b/Tests/Editor/AssetReferenceDrawerTests.cs @@ -124,6 +124,23 @@ internal void SelectionChangedHelper(IList selectedIds) } } + class TestAssetReference : AssetReference + { + internal TestAssetReference(string guid) : base(guid) { } + + internal Object CachedAssetProperty + { + get + { + return CachedAsset; + } + set + { + CachedAsset = value; + } + } + } + public SerializedProperty SetupForSetObjectTests() { // Setup Original AssetReference to not be null @@ -524,6 +541,18 @@ public void AssetReferenceDrawer_SetObject_CanSetToNull() Assert.AreEqual(string.Empty, m_AssetReferenceDrawer.m_AssetRefObject.AssetGUID); } + [Test] + public void AssetReference_WhenCachedGUIDIsNotEqualToAssetGUID_CachedAssetIsNull() + { + // Setup + string guid = "8888888888888888888"; + var assetRef = new TestAssetReference(guid); + assetRef.CachedAssetProperty = new Object(); + + // Test + Assert.IsTrue(assetRef.CachedAssetProperty == null); + } + #if UNITY_2019_2_OR_NEWER [Test] diff --git a/Tests/Editor/Build/BuildScriptPackedTests.cs b/Tests/Editor/Build/BuildScriptPackedTests.cs index d35e6b82..250c2b8d 100644 --- a/Tests/Editor/Build/BuildScriptPackedTests.cs +++ b/Tests/Editor/Build/BuildScriptPackedTests.cs @@ -79,6 +79,23 @@ public void PackedModeScript_CannotBuildPlayContent() Assert.IsTrue(buildScript.CanBuildData()); } + [Test] + public void WarningIsLogged_WhenAddressableGroupDoesNotContainSchema() + { + var buildScript = ScriptableObject.CreateInstance(); + AddressablesDataBuilderInput input = m_BuilderInput; + var group = input.AddressableSettings.CreateGroup("Invalid Group", false, false, false, + new List()); + + buildScript.BuildData(input); + + LogAssert.Expect(LogType.Warning, $"{group.Name} does not have any associated AddressableAssetGroupSchemas. " + + $"Data from this group will not be included in the build. " + + $"If this is unexpected the AddressableGroup may have become corrupted."); + + input.AddressableSettings.RemoveGroup(group); + } + [Test] public void SetAssetEntriesBundleFileIdToCatalogEntryBundleFileId_ModifiedOnlyAssetEntries_ThatAreIncludedInBuildWriteData() { @@ -538,6 +555,48 @@ void CreateGroupWithAssets(string groupName, int assetEntryCount, out Addressabl } } + [Test] + public void GenerateBuildInputDefinition_WithInternalIdModes_GeneratesExpectedAddresses() + { + var group = m_Settings.CreateGroup("DynamicInternalIdGroup", false, false, false, null, typeof(BundledAssetGroupSchema)); + var entries = new List(); + AddressableAssetEntry e = new AddressableAssetEntry($"abcde", $"addr0", group, false); + e.m_cachedAssetPath = "Assets/DummyPath0.asset"; + entries.Add(e); + + e = new AddressableAssetEntry($"abcdf", $"addr0", group, false); + e.m_cachedAssetPath = "Assets/DummyPath0.asset"; + entries.Add(e); + + e = new AddressableAssetEntry($"abcdg", $"addr0", group, false); + e.m_cachedAssetPath = "Assets/DummyPath0.asset"; + entries.Add(e); + + e = new AddressableAssetEntry($"axcde", $"addr0", group, false); + e.m_cachedAssetPath = "Assets/DummyPath0.asset"; + entries.Add(e); + + var schema = group.GetSchema(); + schema.InternalIdNamingMode = BundledAssetGroupSchema.AssetNamingMode.FullPath; + var bundleBuild = BuildScriptPackedMode.GenerateBuildInputDefinition(entries, "bundle"); + Assert.AreEqual("Assets/DummyPath0.asset", bundleBuild.addressableNames[0]); + + schema.InternalIdNamingMode = BundledAssetGroupSchema.AssetNamingMode.Filename; + bundleBuild = BuildScriptPackedMode.GenerateBuildInputDefinition(entries, "bundle"); + Assert.AreEqual("DummyPath0.asset", bundleBuild.addressableNames[0]); + + schema.InternalIdNamingMode = BundledAssetGroupSchema.AssetNamingMode.GUID; + bundleBuild = BuildScriptPackedMode.GenerateBuildInputDefinition(entries, "bundle"); + Assert.AreEqual("abcde", bundleBuild.addressableNames[0]); + + schema.InternalIdNamingMode = BundledAssetGroupSchema.AssetNamingMode.Dynamic; + bundleBuild = BuildScriptPackedMode.GenerateBuildInputDefinition(entries, "bundle"); + Assert.AreEqual("a", bundleBuild.addressableNames[0]); + Assert.AreEqual("ab", bundleBuild.addressableNames[1]); + Assert.AreEqual("abc", bundleBuild.addressableNames[2]); + Assert.AreEqual("ax", bundleBuild.addressableNames[3]); + } + [Test] public void PrepGroupBundlePacking_WhenEntriesDontExpand_AllAssetEntriesAreReturned([Values] BundledAssetGroupSchema.BundlePackingMode mode) { diff --git a/Tests/Editor/Build/GenerateLocationListsTaskTests.cs b/Tests/Editor/Build/GenerateLocationListsTaskTests.cs index 249ee393..5ddcf5ad 100644 --- a/Tests/Editor/Build/GenerateLocationListsTaskTests.cs +++ b/Tests/Editor/Build/GenerateLocationListsTaskTests.cs @@ -90,6 +90,66 @@ public void WhenAssetLoadsFromBundle_ProviderTypesIncludesBundledAssetProvider() CollectionAssert.Contains(output.ProviderTypes, typeof(BundledAssetProvider)); } + [Test] + public void WhenIncludeGUIDInCatalog_SetFalse_GUIDSNotIncluded() + { + GenerateLocationListsTask.Input input = GenerateDefaultInput(); + AddressableAssetGroup groupX = CreateGroupMappedToBundle(input, "X"); + var schema = groupX.GetSchema(); + var guid = CreateAddressablePrefab(input, "p1", groupX, "fileX"); + input.AddressableAssetEntries = BuildAddressableAssetEntryList(input.Settings); + schema.IncludeGUIDInCatalog = true; + foreach (var l in GenerateLocationListsTask.RunInternal(input).Locations) + if (l.Provider == typeof(BundledAssetProvider).FullName) + CollectionAssert.Contains(l.Keys, guid); + + schema.IncludeGUIDInCatalog = false; + foreach (var l in GenerateLocationListsTask.RunInternal(input).Locations) + if (l.Provider == typeof(BundledAssetProvider).FullName) + CollectionAssert.DoesNotContain(l.Keys, guid); + } + + [Test] + public void WhenIncludeAddressOptionChanged_LocationsKeysAreSetCorrectly() + { + GenerateLocationListsTask.Input input = GenerateDefaultInput(); + AddressableAssetGroup groupX = CreateGroupMappedToBundle(input, "X"); + var schema = groupX.GetSchema(); + var guid = CreateAddressablePrefab(input, "p1", groupX, "fileX"); + input.AddressableAssetEntries = BuildAddressableAssetEntryList(input.Settings); + + schema.IncludeAddressInCatalog = true; + foreach (var l in GenerateLocationListsTask.RunInternal(input).Locations) + if (l.Provider == typeof(BundledAssetProvider).FullName) + CollectionAssert.Contains(l.Keys, "p1"); + + schema.IncludeAddressInCatalog = false; + foreach (var l in GenerateLocationListsTask.RunInternal(input).Locations) + if (l.Provider == typeof(BundledAssetProvider).FullName) + CollectionAssert.DoesNotContain(l.Keys, "p1"); + } + + [Test] + public void WhenIncludeLabelsOptionChanged_LocationsKeysAreSetCorrectly() + { + GenerateLocationListsTask.Input input = GenerateDefaultInput(); + AddressableAssetGroup groupX = CreateGroupMappedToBundle(input, "X"); + var schema = groupX.GetSchema(); + var guid = CreateAddressablePrefab(input, "p1", groupX, "fileX"); + groupX.GetAssetEntry(guid).SetLabel("LABEL1", true, true, true); + input.AddressableAssetEntries = BuildAddressableAssetEntryList(input.Settings); + + schema.IncludeLabelsInCatalog = true; + foreach (var l in GenerateLocationListsTask.RunInternal(input).Locations) + if (l.Provider == typeof(BundledAssetProvider).FullName) + CollectionAssert.Contains(l.Keys, "LABEL1"); + + schema.IncludeLabelsInCatalog = false; + foreach (var l in GenerateLocationListsTask.RunInternal(input).Locations) + if(l.Provider == typeof(BundledAssetProvider).FullName) + CollectionAssert.DoesNotContain(l.Keys, "LABEL1"); + } + [Test] public void WhenGroupCreatesMultipleBundles_AllBundlesInAssetGroupToBundlesMap() { diff --git a/Tests/Editor/ContentCatalogTests.cs b/Tests/Editor/ContentCatalogTests.cs index 1c43f361..4de1490e 100644 --- a/Tests/Editor/ContentCatalogTests.cs +++ b/Tests/Editor/ContentCatalogTests.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using UnityEngine; using UnityEngine.AddressableAssets.ResourceLocators; +using UnityEngine.AddressableAssets.Utility; using UnityEngine.ResourceManagement.ResourceLocations; using UnityEngine.ResourceManagement.ResourceProviders; @@ -112,7 +113,8 @@ public void AssetBundleRequestOptionsTest() var dataEntry = new ContentCatalogDataEntry(typeof(ContentCatalogData), "internalId", "provider", new object[] { 1 }, null, options); var entries = new List(); entries.Add(dataEntry); - var ccData = new ContentCatalogData(entries, "TestCatalog"); + var ccData = new ContentCatalogData("TestCatalog"); + ccData.SetData(entries, false); var locator = ccData.CreateLocator(); IList locations; if (!locator.Locate(1, typeof(object), out locations)) @@ -287,5 +289,64 @@ public void VerifyEnumerableHashCalculation() var hash3 = ContentCatalogData.GetHashCodeForEnumerable(dummyValues); Assert.AreNotEqual(hash1, hash3); } + + + [TestCase("0#b", "ab", new string[] { "a" })] + [TestCase("1#b", "bb", new string[] { "a", "b" })] + [TestCase("b", "b", new string[] {"a" })] + [TestCase("b", "b", new string[] { })] + [TestCase("b", "b", null)] + [TestCase("x#b", "x#b", new string[] { "a" })] + [Test] + public void ContentCatalogData_ExpandInternalId_GeneratesExpectedResults(string input, string expected, string[] prefixes) + { + Assert.AreEqual(expected, ContentCatalogData.ExpandInternalId(prefixes, input)); + } + + [Test] + public void SerializationUtility_ReadWrite_Int32() + { + var data = new byte[100]; + for (int i = 0; i < 1000; i++) + { + var val = Random.Range(int.MinValue, int.MaxValue); + var off = Random.Range(0, data.Length - sizeof(int)); + Assert.AreEqual(off + sizeof(int), SerializationUtilities.WriteInt32ToByteArray(data, val, off)); + Assert.AreEqual(val, SerializationUtilities.ReadInt32FromByteArray(data, off)); + } + } + + [Test] + public void ExtractCommonPrefix_ReturnsExpectedString() + { + var prefixes = new List(); + var prefixIndices = new Dictionary(); + Assert.AreEqual("0#/z.ext", ContentCatalogData.ExtractCommonPrefix(prefixes, prefixIndices, "x/y/z.ext")); + Assert.AreEqual("1#/z.ext", ContentCatalogData.ExtractCommonPrefix(prefixes, prefixIndices, "x/z.ext")); + Assert.AreEqual("2#/z.ext", ContentCatalogData.ExtractCommonPrefix(prefixes, prefixIndices, "x/b/z.ext")); + Assert.AreEqual("0#/z.ext", ContentCatalogData.ExtractCommonPrefix(prefixes, prefixIndices, "x/y/z.ext")); + Assert.AreEqual("z.ext", ContentCatalogData.ExtractCommonPrefix(prefixes, prefixIndices, "z.ext")); + Assert.AreEqual(3, prefixes.Count); + } + + string testData = @"{""m_LocatorId"":""AddressablesMainContentCatalog"",""m_InstanceProviderData"":{""m_Id"":""UnityEngine.ResourceManagement.ResourceProviders.InstanceProvider"",""m_ObjectType"":{""m_AssemblyName"":""Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"",""m_ClassName"":""UnityEngine.ResourceManagement.ResourceProviders.InstanceProvider""},""m_Data"":""""},""m_SceneProviderData"":{""m_Id"":""UnityEngine.ResourceManagement.ResourceProviders.SceneProvider"",""m_ObjectType"":{""m_AssemblyName"":""Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"",""m_ClassName"":""UnityEngine.ResourceManagement.ResourceProviders.SceneProvider""},""m_Data"":""""},""m_ResourceProviderData"":[{""m_Id"":""UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider"",""m_ObjectType"":{""m_AssemblyName"":""Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"",""m_ClassName"":""UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider""},""m_Data"":""""},{""m_Id"":""UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider"",""m_ObjectType"":{""m_AssemblyName"":""Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"",""m_ClassName"":""UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider""},""m_Data"":""""},{""m_Id"":""UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider"",""m_ObjectType"":{""m_AssemblyName"":""Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"",""m_ClassName"":""UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider""},""m_Data"":""""}],""m_ProviderIds"":[""UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider"",""UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider""],""m_InternalIds"":[""{UnityEngine.AddressableAssets.Addressables.RuntimePath}/StandaloneWindows64/defaultlocalgroup_assets_all_d4ed3973c342e6f06795a0f8daaebaad.bundle"",""{UnityEngine.AddressableAssets.Addressables.RuntimePath}/StandaloneWindows64/defaultlocalgroup_unitybuiltinshaders_8f144cd21867dc83f60ecd3c93095b52.bundle"",""{UnityEngine.AddressableAssets.Addressables.RuntimePath}/StandaloneWindows64/defaultlocalgroup_scenes_all_e91ebe7804da861b4deb67a340282541.bundle"",""Assets/New Material.mat"",""Assets/swef.unity""],""m_KeyDataString"":""CQAAAABEAAAAZGVmYXVsdGxvY2FsZ3JvdXBfYXNzZXRzX2FsbF9kNGVkMzk3M2MzNDJlNmYwNjc5NWEwZjhkYWFlYmFhZC5idW5kbGUATQAAAGRlZmF1bHRsb2NhbGdyb3VwX3VuaXR5YnVpbHRpbnNoYWRlcnNfOGYxNDRjZDIxODY3ZGM4M2Y2MGVjZDNjOTMwOTViNTIuYnVuZGxlAEQAAABkZWZhdWx0bG9jYWxncm91cF9zY2VuZXNfYWxsX2U5MWViZTc4MDRkYTg2MWI0ZGViNjdhMzQwMjgyNTQxLmJ1bmRsZQAXAAAAQXNzZXRzL05ldyBNYXRlcmlhbC5tYXQAIAAAADNlN2JmNTA3OTRhNzEyMjQ2YWU0ZGNiZTdhODQyOGM4ABEAAABBc3NldHMvc3dlZi51bml0eQAgAAAAYjY4MDdmODNlMWU0ODc2NGM4MjMyM2ZkNTExZTY0NjgEKToMuAQiVa/u"",""m_BucketDataString"":""CQAAAAQAAAABAAAAAAAAAE0AAAABAAAAAQAAAJ8AAAABAAAAAgAAAOgAAAABAAAAAwAAAAQBAAABAAAAAwAAACkBAAABAAAABAAAAD8BAAABAAAABAAAAGQBAAACAAAAAAAAAAEAAABpAQAAAgAAAAIAAAABAAAA"",""m_EntryDataString"":""BQAAAAAAAAAAAAAA/////wAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAP////8AAAAAhQIAAAEAAAAAAAAAAgAAAAAAAAD/////AAAAADQFAAACAAAAAAAAAAMAAAABAAAABwAAACk6DLj/////AwAAAAEAAAAEAAAAAQAAAAgAAAAiVa/u/////wUAAAACAAAA"",""m_ExtraDataString"":""B0xVbml0eS5SZXNvdXJjZU1hbmFnZXIsIFZlcnNpb249MC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1udWxsSlVuaXR5RW5naW5lLlJlc291cmNlTWFuYWdlbWVudC5SZXNvdXJjZVByb3ZpZGVycy5Bc3NldEJ1bmRsZVJlcXVlc3RPcHRpb25z6AEAAHsAIgBtAF8ASABhAHMAaAAiADoAIgBkADQAZQBkADMAOQA3ADMAYwAzADQAMgBlADYAZgAwADYANwA5ADUAYQAwAGYAOABkAGEAYQBlAGIAYQBhAGQAIgAsACIAbQBfAEMAcgBjACIAOgAyADAAMgAxADcANAA3ADAAOQA5ACwAIgBtAF8AVABpAG0AZQBvAHUAdAAiADoAMAAsACIAbQBfAEMAaAB1AG4AawBlAGQAVAByAGEAbgBzAGYAZQByACIAOgBmAGEAbABzAGUALAAiAG0AXwBSAGUAZABpAHIAZQBjAHQATABpAG0AaQB0ACIAOgAtADEALAAiAG0AXwBSAGUAdAByAHkAQwBvAHUAbgB0ACIAOgAwACwAIgBtAF8AQgB1AG4AZABsAGUATgBhAG0AZQAiADoAIgA5ADIAZAAwAGYAOABiAGMAOQBkAGYAZABjADAAMwBlADEAMABkAGYAMgBmADMAYgAzAGIANABjADgAMgA3AGUAIgAsACIAbQBfAEIAdQBuAGQAbABlAFMAaQB6AGUAIgA6ADIANQAyADgALAAiAG0AXwBVAHMAZQBDAHIAYwBGAG8AcgBDAGEAYwBoAGUAZABCAHUAbgBkAGwAZQBzACIAOgB0AHIAdQBlAH0AB0xVbml0eS5SZXNvdXJjZU1hbmFnZXIsIFZlcnNpb249MC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1udWxsSlVuaXR5RW5naW5lLlJlc291cmNlTWFuYWdlbWVudC5SZXNvdXJjZVByb3ZpZGVycy5Bc3NldEJ1bmRsZVJlcXVlc3RPcHRpb25zEgIAAHsAIgBtAF8ASABhAHMAaAAiADoAIgA4AGYAMQA0ADQAYwBkADIAMQA4ADYANwBkAGMAOAAzAGYANgAwAGUAYwBkADMAYwA5ADMAMAA5ADUAYgA1ADIAIgAsACIAbQBfAEMAcgBjACIAOgAzADgAMQAzADcAMgA0ADgANQA5ACwAIgBtAF8AVABpAG0AZQBvAHUAdAAiADoAMAAsACIAbQBfAEMAaAB1AG4AawBlAGQAVAByAGEAbgBzAGYAZQByACIAOgBmAGEAbABzAGUALAAiAG0AXwBSAGUAZABpAHIAZQBjAHQATABpAG0AaQB0ACIAOgAtADEALAAiAG0AXwBSAGUAdAByAHkAQwBvAHUAbgB0ACIAOgAwACwAIgBtAF8AQgB1AG4AZABsAGUATgBhAG0AZQAiADoAIgBmAGMAOAAyAGEAMAAxAGUAYgAwAGEAMgA0AGIAOQBiAGQAOQBjADAAZQBjADEAZAAzAGEAOQBiADIANgA1ADUAXwB1AG4AaQB0AHkAYgB1AGkAbAB0AGkAbgBzAGgAYQBkAGUAcgBzACIALAAiAG0AXwBCAHUAbgBkAGwAZQBTAGkAegBlACIAOgA0ADQANAA1ADQALAAiAG0AXwBVAHMAZQBDAHIAYwBGAG8AcgBDAGEAYwBoAGUAZABCAHUAbgBkAGwAZQBzACIAOgB0AHIAdQBlAH0AB0xVbml0eS5SZXNvdXJjZU1hbmFnZXIsIFZlcnNpb249MC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1udWxsSlVuaXR5RW5naW5lLlJlc291cmNlTWFuYWdlbWVudC5SZXNvdXJjZVByb3ZpZGVycy5Bc3NldEJ1bmRsZVJlcXVlc3RPcHRpb25z6AEAAHsAIgBtAF8ASABhAHMAaAAiADoAIgBlADkAMQBlAGIAZQA3ADgAMAA0AGQAYQA4ADYAMQBiADQAZABlAGIANgA3AGEAMwA0ADAAMgA4ADIANQA0ADEAIgAsACIAbQBfAEMAcgBjACIAOgAzADQAMAA1ADQAMwA2ADQANQAxACwAIgBtAF8AVABpAG0AZQBvAHUAdAAiADoAMAAsACIAbQBfAEMAaAB1AG4AawBlAGQAVAByAGEAbgBzAGYAZQByACIAOgBmAGEAbABzAGUALAAiAG0AXwBSAGUAZABpAHIAZQBjAHQATABpAG0AaQB0ACIAOgAtADEALAAiAG0AXwBSAGUAdAByAHkAQwBvAHUAbgB0ACIAOgAwACwAIgBtAF8AQgB1AG4AZABsAGUATgBhAG0AZQAiADoAIgA5ADEANwBlADUANQAzAGQAZQBiAGQAOAAyADMAOABkAGMAMgBjADIAZAA2ADIANQBkADAAZgA4ADUAOQA0AGMAIgAsACIAbQBfAEIAdQBuAGQAbABlAFMAaQB6AGUAIgA6ADgANwA4ADIALAAiAG0AXwBVAHMAZQBDAHIAYwBGAG8AcgBDAGEAYwBoAGUAZABCAHUAbgBkAGwAZQBzACIAOgB0AHIAdQBlAH0A"",""m_Keys"":[""defaultlocalgroup_assets_all_d4ed3973c342e6f06795a0f8daaebaad.bundle"",""defaultlocalgroup_unitybuiltinshaders_8f144cd21867dc83f60ecd3c93095b52.bundle"",""defaultlocalgroup_scenes_all_e91ebe7804da861b4deb67a340282541.bundle"",""Assets/New Material.mat"",""3e7bf50794a712246ae4dcbe7a8428c8"",""Assets/swef.unity"",""b6807f83e1e48764c82323fd511e6468"",""-1207158231"",""-290499294""],""m_resourceTypes"":[{""m_AssemblyName"":""Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"",""m_ClassName"":""UnityEngine.ResourceManagement.ResourceProviders.IAssetBundleResource""},{""m_AssemblyName"":""UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"",""m_ClassName"":""UnityEngine.Material""},{""m_AssemblyName"":""Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"",""m_ClassName"":""UnityEngine.ResourceManagement.ResourceProviders.SceneInstance""}]}"; + [Test] + public void CanLoad_OldCatalogFormat() + { + var ccd = JsonUtility.FromJson(testData); + Assert.IsNotNull(ccd); + var loc = ccd.CreateLocator(); + Assert.IsNotNull(loc); + Assert.AreEqual(9, loc.Keys.Count()); + foreach (var k in loc.Keys) + { + Assert.IsTrue(loc.Locate(k, null, out var res)); + Assert.IsNotEmpty(res[0].PrimaryKey); + Assert.IsNotEmpty(res[0].InternalId); + Assert.IsNotEmpty(res[0].ProviderId); + Assert.IsNotNull(res[0].ResourceType); + } + + } } } diff --git a/Tests/Runtime/AddressablesIntegrationTestsImpl.cs b/Tests/Runtime/AddressablesIntegrationTestsImpl.cs index e909e661..8f3e87db 100644 --- a/Tests/Runtime/AddressablesIntegrationTestsImpl.cs +++ b/Tests/Runtime/AddressablesIntegrationTestsImpl.cs @@ -240,6 +240,106 @@ public IEnumerator VerifyChainOpPercentCompleteCalculation() op.Release(); } + [UnityTest] + public IEnumerator LoadResourceLocationsAsync_ReturnsCorrectNumberOfLocationsForStringKey() + { + yield return Init(); + + var handle = m_Addressables.LoadResourceLocationsAsync("assetWithDifferentTypedSubAssets"); + yield return handle; + + Assert.AreEqual(3, handle.Result.Count); + HashSet typesSeen = new HashSet(); + foreach (var result in handle.Result) + { + Assert.IsNotNull(result.ResourceType); + typesSeen.Add(result.ResourceType); + } + Assert.AreEqual(3, typesSeen.Count); + m_Addressables.Release(handle); + } + + [UnityTest] + public IEnumerator LoadResourceLocationsAsync_ReturnsCorrectNumberOfLocationsForSubStringKey() + { + yield return Init(); + + var handle = m_Addressables.LoadResourceLocationsAsync("assetWithDifferentTypedSubAssets[Mesh]"); + yield return handle; + + Assert.AreEqual(3, handle.Result.Count); + HashSet typesSeen = new HashSet(); + foreach (var result in handle.Result) + { + Assert.IsNotNull(result.ResourceType); + typesSeen.Add(result.ResourceType); + } + Assert.AreEqual(3, typesSeen.Count); + m_Addressables.Release(handle); + } + + [UnityTest] + public IEnumerator LoadResourceLocationsAsync_ReturnsCorrectNumberOfLocationsForSubStringKey_WhenTypeIsPassedIn() + { + yield return Init(); + + var handle = m_Addressables.LoadResourceLocationsAsync("assetWithDifferentTypedSubAssets[Mesh]", typeof(Mesh)); + yield return handle; + + Assert.AreEqual(1, handle.Result.Count); + Assert.AreEqual(typeof(Mesh), handle.Result[0].ResourceType); + + m_Addressables.Release(handle); + } + + [UnityTest] + public IEnumerator LoadResourceLocationsAsync_ReturnsCorrectNumberOfLocationsForAssetReference() + { + yield return Init(); + + AsyncOperationHandle assetReferenceHandle = m_Addressables.InstantiateAsync(AssetReferenceObjectKey); + yield return assetReferenceHandle; + Assert.IsNotNull(assetReferenceHandle.Result as GameObject); + AssetReferenceTestBehavior behavior = + (assetReferenceHandle.Result as GameObject).GetComponent(); + + var handle = m_Addressables.LoadResourceLocationsAsync(behavior.ReferenceWithMultiTypedSubObject); + yield return handle; + + Assert.AreEqual(3, handle.Result.Count); + HashSet typesSeen = new HashSet(); + foreach (var result in handle.Result) + { + Assert.IsNotNull(result.ResourceType); + typesSeen.Add(result.ResourceType); + } + Assert.AreEqual(3, typesSeen.Count); + + m_Addressables.Release(assetReferenceHandle); + m_Addressables.Release(handle); + } + + [UnityTest] + public IEnumerator LoadResourceLocationsAsync_ReturnsCorrectNumberOfLocationsForSubAssetReference() + { + yield return Init(); + + AsyncOperationHandle assetReferenceHandle = m_Addressables.InstantiateAsync(AssetReferenceObjectKey); + yield return assetReferenceHandle; + Assert.IsNotNull(assetReferenceHandle.Result as GameObject); + AssetReferenceTestBehavior behavior = + (assetReferenceHandle.Result as GameObject).GetComponent(); + + var handle = m_Addressables.LoadResourceLocationsAsync(behavior.ReferenceWithMultiTypedSubObjectSubReference); + yield return handle; + + Assert.AreEqual(1, handle.Result.Count); + Assert.AreEqual(typeof(Material), handle.Result[0].ResourceType); + + m_Addressables.Release(assetReferenceHandle); + m_Addressables.Release(handle); + } + [UnityTest] public IEnumerator PercentComplete_NeverHasDecreasedValue_WhenLoadingAsset() { @@ -282,18 +382,33 @@ public IEnumerator LoadContentCatalogAsync_SetsUpLocalAndRemoteLocations() public IEnumerator LoadingContentCatalogTwice_DoesNotThrowException_WhenHandleIsntReleased() { yield return Init(); + + string fullRemotePath = Path.Combine(kCatalogFolderPath, kCatalogRemotePath); + Directory.CreateDirectory(kCatalogFolderPath); if (m_Addressables.m_ResourceLocators[0].CatalogLocation == null) { +#if UNITY_EDITOR + ContentCatalogData data = new ContentCatalogData(new List{ + new ContentCatalogDataEntry(typeof(string), "testString", "test.provider", new []{"key"}) + }, "test_catalog"); + File.WriteAllText(fullRemotePath, JsonUtility.ToJson(data)); +#else UnityEngine.Debug.Log($"Skipping test {nameof(LoadingContentCatalogTwice_DoesNotThrowException_WhenHandleIsntReleased)} due to missing CatalogLocation."); yield break; +#endif } - - var location = m_Addressables.m_ResourceLocators[0].CatalogLocation; - - var op1 = m_Addressables.LoadContentCatalogAsync(location.InternalId, false); + else + { + string baseCatalogPath = m_Addressables.m_ResourceLocators[0].CatalogLocation.InternalId; + if (baseCatalogPath.StartsWith("file://")) + baseCatalogPath = new Uri(m_Addressables.m_ResourceLocators[0].CatalogLocation.InternalId).AbsolutePath; + File.Copy(baseCatalogPath, fullRemotePath); + } + + var op1 = m_Addressables.LoadContentCatalogAsync(fullRemotePath, false); yield return op1; - var op2 = m_Addressables.LoadContentCatalogAsync(location.InternalId, false); + var op2 = m_Addressables.LoadContentCatalogAsync(fullRemotePath, false); yield return op2; Assert.AreEqual(AsyncOperationStatus.Succeeded, op1.Status); @@ -301,23 +416,42 @@ public IEnumerator LoadingContentCatalogTwice_DoesNotThrowException_WhenHandleIs m_Addressables.Release(op1); m_Addressables.Release(op2); + if (Directory.Exists(kCatalogFolderPath)) + Directory.Delete(kCatalogFolderPath, true); } [UnityTest] public IEnumerator LoadingContentCatalogWithCacheTwice_DoesNotThrowException_WhenHandleIsntReleased() { yield return Init(); + + string fullRemotePath = Path.Combine(kCatalogFolderPath, kCatalogRemotePath); + Directory.CreateDirectory(kCatalogFolderPath); if (m_Addressables.m_ResourceLocators[0].CatalogLocation == null) { +#if UNITY_EDITOR + ContentCatalogData data = new ContentCatalogData(new List{ + new ContentCatalogDataEntry(typeof(string), "testString", "test.provider", new []{"key"}) + }, "test_catalog"); + File.WriteAllText(fullRemotePath, JsonUtility.ToJson(data)); +#else UnityEngine.Debug.Log($"Skipping test {nameof(LoadingContentCatalogWithCacheTwice_DoesNotThrowException_WhenHandleIsntReleased)} due to missing CatalogLocation."); yield break; +#endif } - var location = m_Addressables.m_ResourceLocators[0].CatalogLocation; - - var op1 = m_Addressables.LoadContentCatalogAsync(location.InternalId, false); + else + { + string baseCatalogPath = m_Addressables.m_ResourceLocators[0].CatalogLocation.InternalId; + if (baseCatalogPath.StartsWith("file://")) + baseCatalogPath = new Uri(m_Addressables.m_ResourceLocators[0].CatalogLocation.InternalId).AbsolutePath; + File.Copy(baseCatalogPath, fullRemotePath); + } + WriteHashFileForCatalog(fullRemotePath, "123"); + + var op1 = m_Addressables.LoadContentCatalogAsync(fullRemotePath, false); yield return op1; - var op2 = m_Addressables.LoadContentCatalogAsync(location.InternalId, false); + var op2 = m_Addressables.LoadContentCatalogAsync(fullRemotePath, false); yield return op2; Assert.AreEqual(AsyncOperationStatus.Succeeded, op1.Status); @@ -325,6 +459,8 @@ public IEnumerator LoadingContentCatalogWithCacheTwice_DoesNotThrowException_Whe m_Addressables.Release(op1); m_Addressables.Release(op2); + if (Directory.Exists(kCatalogFolderPath)) + Directory.Delete(kCatalogFolderPath, true); } [UnityTest] @@ -359,25 +495,35 @@ private string WriteHashFileForCatalog(string catalogPath, string hash) public IEnumerator LoadingContentCatalog_CachesCatalogData_IfValidHashFound() { yield return Init(); + + string fullRemotePath = Path.Combine(kCatalogFolderPath, kCatalogRemotePath); + Directory.CreateDirectory(kCatalogFolderPath); if (m_Addressables.m_ResourceLocators[0].CatalogLocation == null) { +#if UNITY_EDITOR + ContentCatalogData data = new ContentCatalogData(new List{ + new ContentCatalogDataEntry(typeof(string), "testString", "test.provider", new []{"key"}) + }, "test_catalog"); + File.WriteAllText(fullRemotePath, JsonUtility.ToJson(data)); +#else UnityEngine.Debug.Log($"Skipping test {nameof(LoadingContentCatalog_CachesCatalogData_IfValidHashFound)} due to missing CatalogLocation."); yield break; +#endif } - Directory.CreateDirectory(kCatalogFolderPath); - string fullRemotePath = Path.Combine(kCatalogFolderPath, kCatalogRemotePath); - string cachedDataPath = m_Addressables.ResolveInternalId(AddressablesImpl.kCacheDataFolder + Path.GetFileName(kCatalogRemotePath)); - string cachedHashPath = cachedDataPath.Replace(".json", ".hash"); - string remoteHashPath = WriteHashFileForCatalog(fullRemotePath, "123"); - - string baseCatalogPath = m_Addressables.m_ResourceLocators[0].CatalogLocation.InternalId; - if (baseCatalogPath.StartsWith("file://")) - baseCatalogPath = new Uri(m_Addressables.m_ResourceLocators[0].CatalogLocation.InternalId).AbsolutePath; - File.Copy(baseCatalogPath, fullRemotePath); + else + { + string baseCatalogPath = m_Addressables.m_ResourceLocators[0].CatalogLocation.InternalId; + if (baseCatalogPath.StartsWith("file://")) + baseCatalogPath = new Uri(m_Addressables.m_ResourceLocators[0].CatalogLocation.InternalId).AbsolutePath; + File.Copy(baseCatalogPath, fullRemotePath); + } + WriteHashFileForCatalog(fullRemotePath, "123"); var op1 = m_Addressables.LoadContentCatalogAsync(fullRemotePath, false); yield return op1; + string cachedDataPath = m_Addressables.ResolveInternalId(AddressablesImpl.kCacheDataFolder + Path.GetFileName(kCatalogRemotePath)); + string cachedHashPath = cachedDataPath.Replace(".json", ".hash"); Assert.IsTrue(File.Exists(cachedDataPath)); Assert.IsTrue(File.Exists(cachedHashPath)); Assert.AreEqual("123", File.ReadAllText(cachedHashPath)); @@ -387,6 +533,54 @@ public IEnumerator LoadingContentCatalog_CachesCatalogData_IfValidHashFound() File.Delete(cachedDataPath); File.Delete(cachedHashPath); } + + [UnityTest] + public IEnumerator LoadingContentCatalog_IfNoCachedHashFound_Succeeds() + { + yield return Init(); + + string fullRemotePath = Path.Combine(kCatalogFolderPath, kCatalogRemotePath); + Directory.CreateDirectory(kCatalogFolderPath); + if (m_Addressables.m_ResourceLocators[0].CatalogLocation == null) + { +#if UNITY_EDITOR + ContentCatalogData data = new ContentCatalogData(new List{ + new ContentCatalogDataEntry(typeof(string), "testString", "test.provider", new []{"key"}) + }, "test_catalog"); + File.WriteAllText(fullRemotePath, JsonUtility.ToJson(data)); +#else + UnityEngine.Debug.Log($"Skipping test {nameof(LoadingContentCatalog_IfNoCachedHashFound_Succeeds)} due to missing CatalogLocation."); + yield break; +#endif + } + else + { + string baseCatalogPath = m_Addressables.m_ResourceLocators[0].CatalogLocation.InternalId; + if (baseCatalogPath.StartsWith("file://")) + baseCatalogPath = new Uri(m_Addressables.m_ResourceLocators[0].CatalogLocation.InternalId).AbsolutePath; + File.Copy(baseCatalogPath, fullRemotePath); + } + WriteHashFileForCatalog(fullRemotePath, "123"); + + string cachedDataPath = m_Addressables.ResolveInternalId(AddressablesImpl.kCacheDataFolder + Path.GetFileName(kCatalogRemotePath)); + string cachedHashPath = cachedDataPath.Replace(".json", ".hash"); + if (File.Exists(cachedDataPath)) + File.Delete(cachedDataPath); + if (File.Exists(cachedHashPath)) + File.Delete(cachedHashPath); + var op1 = m_Addressables.LoadContentCatalogAsync(fullRemotePath, false); + yield return op1; + + Assert.IsTrue(op1.IsValid()); + Assert.AreEqual(AsyncOperationStatus.Succeeded, op1.Status); + Assert.NotNull(op1.Result); + + // Cleanup + Addressables.Release(op1); + Directory.Delete(kCatalogFolderPath, true); + File.Delete(cachedDataPath); + File.Delete(cachedHashPath); + } [UnityTest] public IEnumerator IResourceLocationComparing_SameKeySameTypeDifferentInternalId_ReturnsFalse() @@ -521,7 +715,6 @@ internal bool CatalogDataWasCleaned(ContentCatalogData data) string.IsNullOrEmpty(data.m_EntryDataString) && string.IsNullOrEmpty(data.m_ExtraDataString) && data.m_InternalIds == null && - data.m_Keys == null && string.IsNullOrEmpty(data.m_LocatorId) && data.m_ProviderIds == null && data.m_ResourceProviderData == null && @@ -2012,19 +2205,19 @@ public IEnumerator AssetBundleResource_RemovesCachedBundle_OnLoadFailure() #if ENABLE_CACHING yield return Init(); string bundleName = "bundleName"; - string hash = "123456"; + Hash128 hash = Hash128.Parse("123456"); uint crc = 1; AssetBundleResource abr = new AssetBundleResource(); abr.m_ProvideHandle = new ProvideHandle(m_Addressables.ResourceManager, new ProviderOperation()); abr.m_Options = new AssetBundleRequestOptions() { BundleName = bundleName, - Hash = hash, + Hash = hash.ToString(), Crc = crc, RetryCount = 3 }; - CreateFakeCachedBundle(bundleName, Hash128.Parse(hash).ToString()); - CachedAssetBundle cab = new CachedAssetBundle(bundleName, Hash128.Parse(hash)); + CreateFakeCachedBundle(bundleName, hash.ToString()); + CachedAssetBundle cab = new CachedAssetBundle(bundleName, hash); var request = abr.CreateWebRequest(new ResourceLocationBase("testName", bundleName, typeof(AssetBundleProvider).FullName, typeof(IAssetBundleResource))); @@ -2043,19 +2236,19 @@ public IEnumerator AssetBundleResource_RemovesCachedBundle_OnLoadFailure_WhenRet #if ENABLE_CACHING yield return Init(); string bundleName = "bundleName"; - string hash = "123456"; + Hash128 hash = Hash128.Parse("123456"); uint crc = 1; AssetBundleResource abr = new AssetBundleResource(); abr.m_ProvideHandle = new ProvideHandle(m_Addressables.ResourceManager, new ProviderOperation()); abr.m_Options = new AssetBundleRequestOptions() { BundleName = bundleName, - Hash = hash, + Hash = hash.ToString(), Crc = crc, RetryCount = 0 }; - CreateFakeCachedBundle(bundleName, Hash128.Parse(hash).ToString()); - CachedAssetBundle cab = new CachedAssetBundle(bundleName, Hash128.Parse(hash)); + CreateFakeCachedBundle(bundleName, hash.ToString()); + CachedAssetBundle cab = new CachedAssetBundle(bundleName, hash); var request = abr.CreateWebRequest(new ResourceLocationBase("testName", bundleName, typeof(AssetBundleProvider).FullName, typeof(IAssetBundleResource))); diff --git a/Tests/Runtime/AddressablesTestUtilities.cs b/Tests/Runtime/AddressablesTestUtilities.cs index c9e8b63c..31538665 100644 --- a/Tests/Runtime/AddressablesTestUtilities.cs +++ b/Tests/Runtime/AddressablesTestUtilities.cs @@ -114,9 +114,28 @@ static public void Setup(string testType, string pathFormat, string suffix) }; string hasBehaviorPath = RootFolder + "/AssetReferenceBehavior.prefab"; + + //AssetDatabase.StopAssetEditing(); + + ScriptableObject assetWithDifferentTypedSubAssets = ScriptableObject.CreateInstance(); + AssetDatabase.CreateAsset(assetWithDifferentTypedSubAssets, $"{RootFolder}/assetWithDifferentTypedSubAssets.asset"); + + Material mat = new Material(Shader.Find("Transparent/Diffuse")); + Mesh mesh = new Mesh(); + AssetDatabase.AddObjectToAsset(mat, assetWithDifferentTypedSubAssets); + AssetDatabase.AddObjectToAsset(mesh, assetWithDifferentTypedSubAssets); + + AssetDatabase.ImportAsset($"{RootFolder}/assetWithDifferentTypedSubAssets.asset", ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate); + var assetWithDifferentTypedSubObjectsGUID = AssetDatabase.AssetPathToGUID($"{RootFolder}/assetWithDifferentTypedSubAssets.asset"); + var multiTypedSubAssetsEntry = settings.CreateOrMoveEntry(assetWithDifferentTypedSubObjectsGUID, settings.DefaultGroup); + multiTypedSubAssetsEntry.address = "assetWithDifferentTypedSubAssets"; + aRefTestBehavior.ReferenceWithMultiTypedSubObject = settings.CreateAssetReference(multiTypedSubAssetsEntry.guid); + aRefTestBehavior.ReferenceWithMultiTypedSubObjectSubReference = settings.CreateAssetReference(multiTypedSubAssetsEntry.guid); + aRefTestBehavior.ReferenceWithMultiTypedSubObjectSubReference.SetEditorSubObject(mat); + PrefabUtility.SaveAsPrefabAsset(go, hasBehaviorPath); settings.CreateOrMoveEntry(AssetDatabase.AssetPathToGUID(hasBehaviorPath), group, false, false); - //AssetDatabase.StopAssetEditing(); + AssetDatabase.SaveAssets(); AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate); diff --git a/Tests/Runtime/AssetReferenceTestBehavior.cs b/Tests/Runtime/AssetReferenceTestBehavior.cs index eaa65edc..caae9a60 100644 --- a/Tests/Runtime/AssetReferenceTestBehavior.cs +++ b/Tests/Runtime/AssetReferenceTestBehavior.cs @@ -8,6 +8,8 @@ public class AssetReferenceTestBehavior : MonoBehaviour public AssetReference Reference; public AssetReference InValidAssetReference; public AssetReference ReferenceWithSubObject; + public AssetReference ReferenceWithMultiTypedSubObject; + public AssetReference ReferenceWithMultiTypedSubObjectSubReference; public AssetLabelReference LabelReference; public AssetLabelReference InvalidLabelReference; diff --git a/Tests/Runtime/DynamicResourceLocationTests.cs b/Tests/Runtime/DynamicResourceLocationTests.cs index 1c07b4d7..eb044d31 100644 --- a/Tests/Runtime/DynamicResourceLocationTests.cs +++ b/Tests/Runtime/DynamicResourceLocationTests.cs @@ -4,6 +4,7 @@ using UnityEngine.ResourceManagement.ResourceLocations; using UnityEngine.U2D; using System; +using UnityEngine.ResourceManagement.ResourceProviders; using UnityEngine.TestTools; namespace UnityEngine.AddressableAssets.DynamicResourceLocators @@ -71,6 +72,52 @@ public void GetResourceLocations_WhenLocationExistsAndTypeIsCorrect_LocationIsRe Assert.AreEqual(typeof(GameObject), l.ResourceType); } + [Test] + public void CreateDynamicLocations_CreatesLocationsWithResourceTypes() + { + //Setup + DynamicResourceLocator locator = new DynamicResourceLocator(m_Addressables); + List locations = new List(); + IResourceLocation location = new ResourceLocationBase("test", "test", typeof(BundledAssetProvider).FullName, typeof(GameObject)); + + //Test + locator.CreateDynamicLocations(null, locations, "test", "test", location); + + //Assert + Assert.AreEqual(typeof(GameObject), locations[0].ResourceType); + } + + [Test] + public void CreateDynamicLocations_WithDepdencies_CreatesLocationsWithResourceTypes() + { + //Setup + DynamicResourceLocator locator = new DynamicResourceLocator(m_Addressables); + List locations = new List(); + IResourceLocation depLocation = new ResourceLocationBase("dep1", "dep1", typeof(BundledAssetProvider).FullName, typeof(Texture2D)); + IResourceLocation location = new ResourceLocationBase("test", "test", typeof(BundledAssetProvider).FullName, typeof(GameObject), depLocation); + + //Test + locator.CreateDynamicLocations(null, locations, "test", "test", location); + + //Assert + Assert.AreEqual(typeof(GameObject), locations[0].ResourceType); + } + + [Test] + public void CreateDynamicLocations_WithSpriteAtlas_CreatesLocationsSpriteResourceTypes() + { + //Setup + DynamicResourceLocator locator = new DynamicResourceLocator(m_Addressables); + List locations = new List(); + IResourceLocation location = new ResourceLocationBase("test", "test", typeof(BundledAssetProvider).FullName, typeof(U2D.SpriteAtlas)); + + //Test + locator.CreateDynamicLocations(typeof(Sprite), locations, "test", "test", location); + + //Assert + Assert.AreEqual(typeof(Sprite), locations[0].ResourceType); + } + [Test] public void GetResourceLocations_WithInvalidMainKey_DoesNotReturnALocation() { diff --git a/Tests/Runtime/ResourceManager/Operations/BaseOperationBehaviorTests.cs b/Tests/Runtime/ResourceManager/Operations/BaseOperationBehaviorTests.cs index 99b05f1e..4f5d924c 100644 --- a/Tests/Runtime/ResourceManager/Operations/BaseOperationBehaviorTests.cs +++ b/Tests/Runtime/ResourceManager/Operations/BaseOperationBehaviorTests.cs @@ -162,16 +162,6 @@ public void AsyncOperationHandle_EventSubscriptions_UnsubscribingToNonSubbedEven handle.Destroyed -= oph => {}; Assert.False(op.DestroyedEventHasListeners); - Action dummy = oph => {}; - Assert.False(op.CompletedTypelessEventHasListeners); - - handle.CompletedTypeless += dummy; - Assert.True(op.CompletedTypelessEventHasListeners); - - handle.CompletedTypeless -= dummy; - handle.CompletedTypeless -= dummy; - Assert.False(op.CompletedTypelessEventHasListeners); - handle.Release(); } @@ -287,5 +277,26 @@ public void GroupOperation_WithDuplicateOpThatImplementGetDownloadStatus_DoesNot AssertExpectedDownloadStatus(gOp.GetDownloadStatus(), 1024, 1024, 1f); m_RM.Release(gOp); } + + class TestOp : AsyncOperationBase + { + protected override void Execute() + { + InvokeCompletionEvent(); + } + } + + [Test] + public void CompletionEvents_AreInvoked_InOrderAdded() + { + var op = new TestOp(); + int count = 0; + op.Completed += o => { Assert.AreEqual(0, count); count++; }; + op.CompletedTypeless += o => { Assert.AreEqual(1, count); count++; }; + op.Completed += o => { Assert.AreEqual(2, count); count++; }; + op.CompletedTypeless += o => { Assert.AreEqual(3, count); count++; }; + op.Start(null, default, null); + op.Complete(1, true, null); + } } } diff --git a/package.json b/package.json index 47615116..f60764f3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "com.unity.addressables", "displayName": "Addressables", - "version": "1.16.7", + "version": "1.16.8", "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": [ @@ -20,9 +20,9 @@ "repository": { "url": "https://github.cds.internal.unity3d.com/unity/Addressables.git", "type": "git", - "revision": "91e25bfd2631ef0d729a689d95c0eed344dea37f" + "revision": "defa3df5b741534bf61c24efd27085fb9371dc49" }, "upmCi": { - "footprint": "209337fd0fef2f8169885004ebb9b1cf75295a6a" + "footprint": "f4e4e785f695a55b72fc9671784d3c363e317d1b" } }