using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Runtime.Serialization.Formatters.Binary; using System.Text; using UnityEditor.AddressableAssets.Build.DataBuilders; using UnityEditor.AddressableAssets.Settings; using UnityEditor.AddressableAssets.Settings.GroupSchemas; using UnityEditor.Build.Content; using UnityEditor.Build.Pipeline.Interfaces; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.AddressableAssets.ResourceLocators; using UnityEngine.ResourceManagement.ResourceProviders; using UnityEngine.ResourceManagement.Util; #if ENABLE_CCD using Unity.Services.Ccd.Management; #endif namespace UnityEditor.AddressableAssets.Build { /// /// Option for how to deal with automatically checking for content update restrictions as part of the Update a Previous Build workflow. /// public enum CheckForContentUpdateRestrictionsOptions { /// /// If assets are modified that have been previously built in a Cannot Change Post Release group, /// the build will be paused and the Update Restrictions Check window is opened /// ListUpdatedAssetsWithRestrictions = 0, /// /// If assets are modified that have been previously built in a Cannot Change Post Release group, the Content Update build will fail. /// FailBuild = 1, /// /// Updating a previous build does not automatically run the Check for Update Restrictions rule. /// Disabled = 2 } #if ENABLE_CCD /// /// This is used to determine the behavior of Update a Previous Build when taking advantage of the Build & Release feature. /// public enum BuildAndReleaseContentStateBehavior { /// /// Uses the Previous Content State bin file path set in the AddressableAssetSettings /// UsePresetLocation = 0, /// /// Pulls the Previous Content State bin from the associated Cloud Content Delivery bucket set in the profile variables. /// UseCCDBucket = 1 } #endif /// /// The given state of an Asset. Represented by its guid and hash. /// [Serializable] public struct AssetState : IEquatable { /// /// Asset states GUID. /// public GUID guid; /// /// Asset State hash. /// public Hash128 hash; /// /// Check if one asset state is equal to another. /// /// Right hand side of comparision. /// Returns true if the Asset States are equal to one another. public bool Equals(AssetState other) { return guid == other.guid && hash == other.hash; } } /// /// The Cached Asset State of an Addressable Asset. /// [Serializable] public class CachedAssetState : IEquatable { /// /// The Asset State. /// public AssetState asset; /// /// The Asset State of all dependencies. /// public AssetState[] dependencies; /// /// The guid for the group the cached asset state belongs to. /// public string groupGuid; /// /// The name of the cached asset states bundle file. /// public string bundleFileId; /// /// The cached asset state data. /// public object data; /// /// Checks if one cached asset state is equal to another given the asset state and dependency state. /// /// Right hand side of comparision. /// Returns true if the cached asset states are equal to one another. public bool Equals(CachedAssetState other) { bool result = other != null && asset.Equals(other.asset); result &= dependencies != null && other.dependencies != null; result &= dependencies.Length == other.dependencies.Length; var index = 0; while (result && index < dependencies.Length) { result &= dependencies[index].Equals(other.dependencies[index]); index++; } return result; } } /// /// Cached state of asset bundles. /// [Serializable] public class CachedBundleState { /// /// The name of the cached asset states bundle file. /// public string bundleFileId; /// /// The cached bundle state data. /// public object data; } /// /// Data stored with each build that is used to generated content updates. /// [Serializable] public class AddressablesContentState { /// /// The version that the player was built with. This is usually set to AddressableAssetSettings.PlayerBuildVersion. /// [SerializeField] public string playerVersion; /// /// The version of the unity editor used to build the player. /// [SerializeField] public string editorVersion; /// /// Dependency information for all assets in the build that have been marked StaticContent. /// [SerializeField] public CachedAssetState[] cachedInfos; /// /// The path of a remote catalog. This is the only place the player knows to look for an updated catalog. /// [SerializeField] public string remoteCatalogLoadPath; /// /// Information about asset bundles created for the build. /// [SerializeField] public CachedBundleState[] cachedBundles; } internal struct ContentUpdateUsageData { public string ContentUpdateInterruptMessage; public bool UsingCCD; } internal struct ContentUpdateBuildData { public string Error; public double BuildDuration; } /// /// Contains methods used for the content update workflow. /// public static class ContentUpdateScript { internal static readonly string FirstTimeUpdatePreviousBuild = nameof(FirstTimeUpdatePreviousBuild); /// /// Contains build information used for updating assets. /// public struct ContentUpdateContext { /// /// The mapping of an asset's guid to its cached asset state. /// public Dictionary GuidToPreviousAssetStateMap; /// /// The mapping of an asset's or bundle's internal id to its catalog entry. /// public Dictionary IdToCatalogDataEntryMap; /// /// The mapping of a bundle's name to its internal bundle id. /// public Dictionary BundleToInternalBundleIdMap; /// /// Stores the asset bundle write information. /// public IBundleWriteData WriteData; /// /// Stores the cached build data. /// public AddressablesContentState ContentState; /// /// Stores the paths of the files created during a build. /// public FileRegistry Registry; /// /// The list of asset state information gathered from the previous build. /// public List PreviousAssetStateCarryOver; } private static string m_BinFileCachePath = "Library/com.unity.addressables/AddressablesBinFileDownload/addressables_content_state.bin"; /// /// If the previous content state file location is a remote location, this path is where the file is downloaded to as part of a /// content update build. In the event of a fresh build where the previous state file build path is remote, this is the location the /// file is built to. /// public static string PreviousContentStateFileCachePath { get { return m_BinFileCachePath; } set { m_BinFileCachePath = value; } } static bool GetAssetState(GUID asset, out AssetState assetState) { assetState = new AssetState(); if (asset.Empty()) return false; var path = AssetDatabase.GUIDToAssetPath(asset.ToString()); if (string.IsNullOrEmpty(path)) return false; var hash = AssetDatabase.GetAssetDependencyHash(path); if (!hash.isValid) return false; assetState.guid = asset; assetState.hash = hash; return true; } static bool GetCachedAssetStateForData(GUID asset, string bundleFileId, string groupGuid, object data, IEnumerable dependencies, out CachedAssetState cachedAssetState) { cachedAssetState = null; AssetState assetState; if (!GetAssetState(asset, out assetState)) return false; var visited = new HashSet(); visited.Add(asset); var dependencyStates = new List(); foreach (var dependency in dependencies) { if (!visited.Add(dependency)) continue; AssetState dependencyState; if (!GetAssetState(dependency, out dependencyState)) continue; dependencyStates.Add(dependencyState); } cachedAssetState = new CachedAssetState(); cachedAssetState.asset = assetState; cachedAssetState.dependencies = dependencyStates.ToArray(); cachedAssetState.groupGuid = groupGuid; cachedAssetState.bundleFileId = bundleFileId; cachedAssetState.data = data; return true; } static bool HasAssetOrDependencyChanged(CachedAssetState cachedInfo) { CachedAssetState newCachedInfo; if (!GetCachedAssetStateForData(cachedInfo.asset.guid, cachedInfo.bundleFileId, cachedInfo.groupGuid, cachedInfo.data, cachedInfo.dependencies.Select(x => x.guid), out newCachedInfo)) return true; return !cachedInfo.Equals(newCachedInfo); } /// /// Save the content update information for a set of AddressableAssetEntry objects. /// /// File to write content stat info to. If file already exists, it will be deleted before the new file is created. /// The entries to save. /// The raw dependency information generated from the build. /// The player version to save. This is usually set to AddressableAssetSettings.PlayerBuildVersion. /// The server path (if any) that contains an updateable content catalog. If this is empty, updates cannot occur. /// True if the file is saved, false otherwise. [Obsolete] public static bool SaveContentState(string path, List entries, IDependencyData dependencyData, string playerVersion, string remoteCatalogPath) { return SaveContentState(new List(), path, entries, dependencyData, playerVersion, remoteCatalogPath); } /// /// Save the content update information for a set of AddressableAssetEntry objects. /// /// The ContentCatalogDataEntry locations that were built into the Content Catalog. /// File to write content stat info to. If file already exists, it will be deleted before the new file is created. /// The entries to save. /// The raw dependency information generated from the build. /// The player version to save. This is usually set to AddressableAssetSettings.PlayerBuildVersion. /// The server path (if any) that contains an updateable content catalog. If this is empty, updates cannot occur. /// True if the file is saved, false otherwise. public static bool SaveContentState(List locations, string path, List entries, IDependencyData dependencyData, string playerVersion, string remoteCatalogPath) { return SaveContentState(locations, path, entries, dependencyData, playerVersion, remoteCatalogPath, null); } /// /// Save the content update information for a set of AddressableAssetEntry objects. /// /// The ContentCatalogDataEntry locations that were built into the Content Catalog. /// File to write content stat info to. If file already exists, it will be deleted before the new file is created. /// The entries to save. /// The raw dependency information generated from the build. /// The player version to save. This is usually set to AddressableAssetSettings.PlayerBuildVersion. /// The server path (if any) that contains an updateable content catalog. If this is empty, updates cannot occur. /// Cached state that needs to carry over from the previous build. This mainly affects Content Update. /// True if the file is saved, false otherwise. public static bool SaveContentState(List locations, string path, List entries, IDependencyData dependencyData, string playerVersion, string remoteCatalogPath, List carryOverCacheState) { return SaveContentState(locations, null, path, entries, dependencyData, playerVersion, remoteCatalogPath, carryOverCacheState); } /// /// Save the content update information for a set of AddressableAssetEntry objects. /// /// The ContentCatalogDataEntry locations that were built into the Content Catalog. /// Mapping of asset Guid to catalog locations entries for lookup of extra data. /// File to write content stat info to. If file already exists, it will be deleted before the new file is created. /// The entries to save. /// The raw dependency information generated from the build. /// The player version to save. This is usually set to AddressableAssetSettings.PlayerBuildVersion. /// The server path (if any) that contains an updateable content catalog. If this is empty, updates cannot occur. /// Cached state that needs to carry over from the previous build. This mainly affects Content Update. /// True if the file is saved, false otherwise. internal static bool SaveContentState(List locations, Dictionary> guidToCatalogLocation, string path, List entries, IDependencyData dependencyData, string playerVersion, string remoteCatalogPath, List carryOverCacheState) { try { var cachedInfos = GetCachedAssetStates(guidToCatalogLocation, entries, dependencyData); var cachedBundleInfos = new List(); foreach (ContentCatalogDataEntry ccEntry in locations) { if (typeof(IAssetBundleResource).IsAssignableFrom(ccEntry.ResourceType)) cachedBundleInfos.Add(new CachedBundleState() {bundleFileId = ccEntry.InternalId, data = ccEntry.Data}); } if (carryOverCacheState != null) { foreach (var cs in carryOverCacheState) cachedInfos.Add(cs); } var cacheData = new AddressablesContentState { cachedInfos = cachedInfos.ToArray(), playerVersion = playerVersion, editorVersion = Application.unityVersion, remoteCatalogLoadPath = remoteCatalogPath, cachedBundles = cachedBundleInfos.ToArray() }; var formatter = new BinaryFormatter(); if (File.Exists(path)) File.Delete(path); var dir = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir); var stream = new FileStream(path, FileMode.CreateNew, FileAccess.Write); formatter.Serialize(stream, cacheData); stream.Flush(); stream.Close(); stream.Dispose(); return true; } catch (UnauthorizedAccessException uae) { if (!AddressableAssetUtility.IsVCAssetOpenForEdit(path)) Debug.LogErrorFormat("Cannot access the file {0}. It may be locked by version control.", path); else Debug.LogException(uae); return false; } catch (Exception e) { Debug.LogException(e); return false; } } static IList GetCachedAssetStates(Dictionary> guidToCatalogLocation, List entries, IDependencyData dependencyData) { IList gatheredCachedInfos = new List(); Dictionary guidToEntries = new Dictionary(); foreach (AddressableAssetEntry entry in entries) if (!guidToEntries.ContainsKey(entry.guid)) guidToEntries[entry.guid] = entry; foreach (KeyValuePair assetData in dependencyData.AssetInfo) GetCachedAssetState(guidToCatalogLocation, guidToEntries, assetData.Key, assetData.Value.referencedObjects, gatheredCachedInfos); foreach (KeyValuePair sceneData in dependencyData.SceneInfo) GetCachedAssetState(guidToCatalogLocation, guidToEntries, sceneData.Key, sceneData.Value.referencedObjects, gatheredCachedInfos); return gatheredCachedInfos; } private static void GetCachedAssetState(Dictionary> guidToCatalogLocation, Dictionary guidToEntries, GUID guid, IReadOnlyCollection dependencies, IList cachedInfos) { guidToEntries.TryGetValue(guid.ToString(), out AddressableAssetEntry addressableEntry); List catalogLocationsForSceneGuid = null; guidToCatalogLocation?.TryGetValue(guid, out catalogLocationsForSceneGuid); if (addressableEntry != null) { object catalogData = catalogLocationsForSceneGuid != null && catalogLocationsForSceneGuid.Count > 0 ? catalogLocationsForSceneGuid[0].Data : null; if (GetCachedAssetStateForData(guid, addressableEntry.BundleFileId, addressableEntry.parentGroup.Guid, catalogData, dependencies.Select(x => x.guid), out CachedAssetState cachedAssetState)) cachedInfos.Add(cachedAssetState); } } /// /// Gets the path of the cache data from a selected build. /// /// If true, the user is allowed to browse for a specific file. /// The path of the previous state .bin file used to detect changes from the previous build to the content update build. public static string GetContentStateDataPath(bool browse) { return GetContentStateDataPath(browse, null); } internal static string GetContentStateDataPath(bool browse, AddressableAssetSettings settings) { if (settings == null) settings = AddressableAssetSettingsDefaultObject.Settings; var profileSettings = settings == null ? null : settings.profileSettings; string assetPath = profileSettings != null ? profileSettings.EvaluateString(settings.activeProfileId, settings.ContentStateBuildPath) : ""; if (string.IsNullOrEmpty(assetPath)) { assetPath = settings != null ? settings.GetContentStateBuildPath() : Path.Combine(AddressableAssetSettingsDefaultObject.kDefaultConfigFolder, PlatformMappingService.GetPlatformPathSubFolder()); } if (browse) { if (string.IsNullOrEmpty(assetPath)) assetPath = Application.dataPath; assetPath = EditorUtility.OpenFilePanel("Build Data File", Path.GetDirectoryName(assetPath), "bin"); if (string.IsNullOrEmpty(assetPath)) return null; return assetPath; } if (!ResourceManagerConfig.ShouldPathUseWebRequest(assetPath)) { try { Directory.CreateDirectory(assetPath); } catch (Exception e) { Debug.LogError(e.Message + "\nCheck \"Content State Build Path\" in Addressables settings. Falling back to config folder location."); assetPath = Path.Combine(AddressableAssetSettingsDefaultObject.kDefaultConfigFolder, PlatformMappingService.GetPlatformPathSubFolder()); Directory.CreateDirectory(assetPath); } } #if ENABLE_CCD switch(settings.BuildAndReleaseBinFileOption) { case BuildAndReleaseContentStateBehavior.UsePresetLocation: //do nothing break; case BuildAndReleaseContentStateBehavior.UseCCDBucket: assetPath = settings.RemoteCatalogLoadPath.GetValue(settings); break; } #endif var path = Path.Combine(assetPath, "addressables_content_state.bin"); return path; } /// /// Downloads the content state bin to a temporary directory /// /// The url of the bin file /// The temp path the bin file was downloaded to. internal static string DownloadBinFileToTempLocation(string url) { if (!Directory.Exists(ContentUpdateScript.PreviousContentStateFileCachePath)) Directory.CreateDirectory(Path.GetDirectoryName(ContentUpdateScript.PreviousContentStateFileCachePath)); else if (File.Exists(ContentUpdateScript.PreviousContentStateFileCachePath)) File.Delete(ContentUpdateScript.PreviousContentStateFileCachePath); try { var bytes = new WebClient().DownloadData(url); File.WriteAllBytes(ContentUpdateScript.PreviousContentStateFileCachePath, bytes); } catch { //Do nothing, nothing will get downloaded and the users can select a file manually if they want. } return ContentUpdateScript.PreviousContentStateFileCachePath; } /// /// Loads cache data from a specific location /// /// /// The ContentState object. public static AddressablesContentState LoadContentState(string contentStateDataPath) { if (string.IsNullOrEmpty(contentStateDataPath)) { Debug.LogErrorFormat("Unable to load cache data from {0}.", contentStateDataPath); return null; } var stream = new FileStream(contentStateDataPath, FileMode.Open, FileAccess.Read); var formatter = new BinaryFormatter(); var cacheData = formatter.Deserialize(stream) as AddressablesContentState; if (cacheData == null) { Addressables.LogError( "Invalid hash data file. This file is usually named addressables_content_state.bin and is saved in the same folder as your source AddressableAssetsSettings.asset file."); return null; } stream.Dispose(); return cacheData; } static bool s_StreamingAssetsExists; static string kStreamingAssetsPath = "Assets/StreamingAssets"; internal static void Cleanup(bool deleteStreamingAssetsFolderIfEmpty, bool cleanBuildPath) { if (cleanBuildPath) { DirectoryUtility.DeleteDirectory(Addressables.BuildPath, onlyIfEmpty: false, recursiveDelete: true); } if (deleteStreamingAssetsFolderIfEmpty) { DirectoryUtility.DeleteDirectory(kStreamingAssetsPath, onlyIfEmpty: true); } } /// /// Builds player content using the player content version from a specified cache file. /// /// The settings object to use for the build. /// The path of the cache data to use. /// The build operation. public static AddressablesPlayerBuildResult BuildContentUpdate(AddressableAssetSettings settings, string contentStateDataPath) { var cacheData = LoadContentState(contentStateDataPath); if (!IsCacheDataValid(settings, cacheData)) return null; s_StreamingAssetsExists = Directory.Exists("Assets/StreamingAssets"); var context = new AddressablesDataBuilderInput(settings, cacheData.playerVersion); context.IsContentUpdateBuild = true; context.PreviousContentState = cacheData; Cleanup(!s_StreamingAssetsExists, false); SceneManagerState.Record(); var result = settings.ActivePlayerDataBuilder.BuildData(context); if (!string.IsNullOrEmpty(result.Error)) Debug.LogError(result.Error); SceneManagerState.Restore(); return result; } internal static bool IsCacheDataValid(AddressableAssetSettings settings, AddressablesContentState cacheData) { if (cacheData == null) return false; if (cacheData.editorVersion != Application.unityVersion) Addressables.LogWarningFormat("Building content update with Unity editor version `{0}`, data was created with version `{1}`. This may result in incompatible data.", Application.unityVersion, cacheData.editorVersion); if (string.IsNullOrEmpty(cacheData.remoteCatalogLoadPath)) { Addressables.LogError("Previous build had 'Build Remote Catalog' disabled. You cannot update a player that has no remote catalog specified"); return false; } if (!settings.BuildRemoteCatalog) { Addressables.LogError("Current settings have 'Build Remote Catalog' disabled. You cannot update a player that has no remote catalog to look to."); return false; } if (cacheData.remoteCatalogLoadPath != settings.RemoteCatalogLoadPath.GetValue(settings)) { Addressables.LogErrorFormat( "Current 'Remote Catalog Load Path' does not match load path of original player. Player will only know to look up catalog at original location. Original: {0} Current: {1}", cacheData.remoteCatalogLoadPath, settings.RemoteCatalogLoadPath.GetValue(settings)); return false; } return true; } /// /// Get all modified addressable asset entries in groups that have BundledAssetGroupSchema and ContentUpdateGroupSchema with static content enabled. /// This includes any Addressable dependencies that are affected by the modified entries. /// /// Addressable asset settings. /// The cache data path. /// A list of all modified entries and dependencies (list is empty if there are none); null if failed to load cache data. public static List GatherModifiedEntries(AddressableAssetSettings settings, string cacheDataPath) { HashSet retVal = new HashSet(); var entriesMap = GatherModifiedEntriesWithDependencies(settings, cacheDataPath); foreach (var entry in entriesMap.Keys) { if (!retVal.Contains(entry)) retVal.Add(entry); foreach (var dependency in entriesMap[entry]) if (!retVal.Contains(dependency)) retVal.Add(dependency); } return retVal.ToList(); } internal static void GatherExplicitModifiedEntries(AddressableAssetSettings settings, ref Dictionary> dependencyMap, AddressablesContentState cacheData) { List noBundledAssetGroupSchema = new List(); List noStaticContent = new List(); var allEntries = new List(); settings.GetAllAssets(allEntries, false, g => { if (g == null) return false; if (!g.HasSchema()) { noBundledAssetGroupSchema.Add(g.Name); return false; } if (!g.HasSchema()) { noStaticContent.Add(g.Name); return false; } if (!g.GetSchema().StaticContent) { noStaticContent.Add(g.Name); return false; } g.FlaggedDuringContentUpdateRestriction = false; return true; }); StringBuilder builder = new StringBuilder(); builder.AppendFormat("Skipping Prepare for Content Update on {0} group(s):\n\n", noBundledAssetGroupSchema.Count + noStaticContent.Count); AddInvalidGroupsToLogMessage(builder, noBundledAssetGroupSchema, "Group Did Not Contain BundledAssetGroupSchema"); AddInvalidGroupsToLogMessage(builder, noStaticContent, "Static Content Not Enabled In Schemas"); Debug.Log(builder.ToString()); var entryToCacheInfo = new Dictionary(); foreach (var cacheInfo in cacheData.cachedInfos) if (cacheInfo != null) entryToCacheInfo[cacheInfo.asset.guid.ToString()] = cacheInfo; var modifiedEntries = new List(); foreach (var entry in allEntries) { if (!entryToCacheInfo.TryGetValue(entry.guid, out CachedAssetState cachedInfo) || HasAssetOrDependencyChanged(cachedInfo)) { Type mainType = AddressableAssetUtility.MapEditorTypeToRuntimeType(entry.MainAssetType, false); if ((mainType == null || mainType == typeof(DefaultAsset)) && !entry.IsInResources) { entry.FlaggedDuringContentUpdateRestriction = false; } else { modifiedEntries.Add(entry); entry.FlaggedDuringContentUpdateRestriction = true; entry.parentGroup.FlaggedDuringContentUpdateRestriction = true; } } else entry.FlaggedDuringContentUpdateRestriction = false; } AddAllDependentScenesFromModifiedEntries(modifiedEntries); foreach (var entry in modifiedEntries) { if (!dependencyMap.ContainsKey(entry)) dependencyMap.Add(entry, new List()); } } internal static void ClearContentUpdateNotifications(AddressableAssetGroup assetGroup) { if (assetGroup == null) return; if (assetGroup.FlaggedDuringContentUpdateRestriction) { ClearContentUpdateFlagForEntries(assetGroup.entries); assetGroup.FlaggedDuringContentUpdateRestriction = false; } } static void ClearContentUpdateFlagForEntries(ICollection entries) { foreach (var e in entries) { if (e != null) e.FlaggedDuringContentUpdateRestriction = false; if (e.IsFolder) { List folderEntries = new List(); e.GatherFolderEntries(folderEntries, true, true, null); ClearContentUpdateFlagForEntries(folderEntries); } } } /// /// Get a Dictionary of all modified values and their dependencies. Dependencies will be Addressable and part of a group /// with static content enabled. /// /// Addressable asset settings. /// The cache data path. /// A dictionary mapping explicit changed entries to their dependencies. public static Dictionary> GatherModifiedEntriesWithDependencies(AddressableAssetSettings settings, string cachePath) { var modifiedData = new Dictionary>(); AddressablesContentState cacheData = LoadContentState(cachePath); if (cacheData == null) return modifiedData; GatherExplicitModifiedEntries(settings, ref modifiedData, cacheData); GetStaticContentDependenciesForEntries(settings, ref modifiedData, GetGroupGuidToCacheBundleNameMap(cacheData)); GetEntriesDependentOnModifiedEntries(settings, ref modifiedData); return modifiedData; } internal static void GetEntriesDependentOnModifiedEntries(AddressableAssetSettings settings, ref Dictionary> dependencyMap) { var groups = GetStaticGroups(settings); Dictionary entryToDependencies = new Dictionary(); foreach (AddressableAssetGroup group in groups) { foreach (AddressableAssetEntry entry in group.entries) { string[] dependencies = AssetDatabase.GetDependencies(entry.AssetPath); entryToDependencies.Add(entry, dependencies); } } HashSet modifiedEntries = new HashSet(); foreach (KeyValuePair> mappedEntry in dependencyMap) { modifiedEntries.Add(mappedEntry.Key); foreach (AddressableAssetEntry dependencyEntry in mappedEntry.Value) modifiedEntries.Add(dependencyEntry); } // if an entry is dependant on a modified entry, then it too should be modified to reference the moved asset foreach (AddressableAssetEntry modifiedEntry in modifiedEntries) { foreach (KeyValuePair dependency in entryToDependencies) { if (dependency.Key != modifiedEntry && dependency.Value.Contains(modifiedEntry.AssetPath) && dependencyMap.TryGetValue(modifiedEntry, out var value)) { if (!value.Contains(dependency.Key)) value.Add(dependency.Key); } } } } internal static List GetStaticGroups(AddressableAssetSettings settings) { List staticGroups = new List(); foreach (AddressableAssetGroup group in settings.groups) { var staticSchema = group.GetSchema(); if (staticSchema == null) continue; var bundleSchema = group.GetSchema(); if (bundleSchema == null) continue; if (staticSchema.StaticContent) staticGroups.Add(group); } return staticGroups; } internal static Dictionary GetGroupGuidToCacheBundleNameMap(AddressablesContentState cacheData) { var bundleIdToCacheInfo = new Dictionary(); foreach (CachedBundleState bundleInfo in cacheData.cachedBundles) { if (bundleInfo != null && bundleInfo.data is AssetBundleRequestOptions options) bundleIdToCacheInfo[bundleInfo.bundleFileId] = options.BundleName; } var groupGuidToCacheBundleName = new Dictionary(); foreach (CachedAssetState cacheInfo in cacheData.cachedInfos) { if (cacheInfo != null && bundleIdToCacheInfo.TryGetValue(cacheInfo.bundleFileId, out string bundleName)) groupGuidToCacheBundleName[cacheInfo.groupGuid] = bundleName; } return groupGuidToCacheBundleName; } internal static HashSet GetGroupGuidsWithUnchangedBundleName(AddressableAssetSettings settings, Dictionary> dependencyMap, Dictionary groupGuidToCacheBundleName) { var result = new HashSet(); if (groupGuidToCacheBundleName == null || groupGuidToCacheBundleName.Count == 0) return result; var entryGuidToDeps = new Dictionary>(); foreach (KeyValuePair> entryToDeps in dependencyMap) { entryGuidToDeps.Add(entryToDeps.Key.guid, entryToDeps.Value); } foreach (AddressableAssetGroup group in settings.groups) { if (group == null || !group.HasSchema()) continue; var schema = group.GetSchema(); List bundleInputDefinitions = new List(); BuildScriptPackedMode.PrepGroupBundlePacking(group, bundleInputDefinitions, schema, entry => !entryGuidToDeps.ContainsKey(entry.guid)); BuildScriptPackedMode.HandleBundleNames(bundleInputDefinitions); for (int i = 0; i < bundleInputDefinitions.Count; i++) { string bundleName = Path.GetFileNameWithoutExtension(bundleInputDefinitions[i].assetBundleName); if (groupGuidToCacheBundleName.TryGetValue(group.Guid, out string cacheBundleName) && cacheBundleName == bundleName) result.Add(group.Guid); } } return result; } internal static void GetStaticContentDependenciesForEntries(AddressableAssetSettings settings, ref Dictionary> dependencyMap, Dictionary groupGuidToCacheBundleName = null) { if (dependencyMap == null) return; Dictionary groupHasStaticContentMap = new Dictionary(); HashSet groupGuidsWithUnchangedBundleName = GetGroupGuidsWithUnchangedBundleName(settings, dependencyMap, groupGuidToCacheBundleName); foreach (AddressableAssetEntry entry in dependencyMap.Keys) { //since the entry here is from our list of modified entries we know that it must be a part of a static content group. //Since it's part of a static content update group we can go ahead and set the value to true in the dictionary without explicitly checking it. if (!groupHasStaticContentMap.ContainsKey(entry.parentGroup)) groupHasStaticContentMap.Add(entry.parentGroup, true); string[] dependencies = AssetDatabase.GetDependencies(entry.AssetPath); foreach (string dependency in dependencies) { string guid = AssetDatabase.AssetPathToGUID(dependency); var depEntry = settings.FindAssetEntry(guid, true); if (depEntry == null) continue; if (groupGuidsWithUnchangedBundleName.Contains(depEntry.parentGroup.Guid)) continue; if (!groupHasStaticContentMap.TryGetValue(depEntry.parentGroup, out bool groupHasStaticContentEnabled)) { groupHasStaticContentEnabled = depEntry.parentGroup.HasSchema() && depEntry.parentGroup.GetSchema().StaticContent; groupHasStaticContentMap.Add(depEntry.parentGroup, groupHasStaticContentEnabled); } if (!dependencyMap.ContainsKey(depEntry) && groupHasStaticContentEnabled) { if (!dependencyMap.ContainsKey(entry)) dependencyMap.Add(entry, new List()); dependencyMap[entry].Add(depEntry); depEntry.FlaggedDuringContentUpdateRestriction = true; } } } } internal static void AddAllDependentScenesFromModifiedEntries(List modifiedEntries) { List entriesToAdd = new List(); //If a scene has changed, all scenes that end up in the same bundle need to be marked as modified due to bundle dependencies foreach (AddressableAssetEntry entry in modifiedEntries) { if (entry.IsScene && !entriesToAdd.Contains(entry)) { switch (entry.parentGroup.GetSchema().BundleMode) { case BundledAssetGroupSchema.BundlePackingMode.PackTogether: //Add every scene in the group to modified entries foreach (AddressableAssetEntry sharedGroupEntry in entry.parentGroup.entries) { if (sharedGroupEntry.IsScene && !modifiedEntries.Contains(sharedGroupEntry)) { sharedGroupEntry.FlaggedDuringContentUpdateRestriction = true; entriesToAdd.Add(sharedGroupEntry); } } break; case BundledAssetGroupSchema.BundlePackingMode.PackTogetherByLabel: foreach (AddressableAssetEntry sharedGroupEntry in entry.parentGroup.entries) { //Check if one entry has 0 labels while the other contains labels. The labels union check below will return true in this case. //That is not the behavior we want. So to avoid that, we check here first. if (sharedGroupEntry.labels.Count == 0 ^ entry.labels.Count == 0) continue; //Only add if labels are shared if (sharedGroupEntry.IsScene && !modifiedEntries.Contains(sharedGroupEntry) && sharedGroupEntry.labels.Union(entry.labels).Any()) { sharedGroupEntry.FlaggedDuringContentUpdateRestriction = true; entriesToAdd.Add(sharedGroupEntry); } } break; case BundledAssetGroupSchema.BundlePackingMode.PackSeparately: //Do nothing. The scene will be in a different bundle. break; default: break; } } } modifiedEntries.AddRange(entriesToAdd); } private static void AddInvalidGroupsToLogMessage(StringBuilder builder, List invalidGroupList, string headerMessage) { if (invalidGroupList.Count > 0) { builder.AppendFormat("{0} ({1} groups):\n", headerMessage, invalidGroupList.Count); int maxList = 15; for (int i = 0; i < invalidGroupList.Count; i++) { if (i > maxList) { builder.AppendLine("..."); break; } builder.AppendLine("-" + invalidGroupList[i]); } builder.AppendLine(""); } } /// /// Create a new AddressableAssetGroup with the items and mark it as remote. /// /// The settings object. /// The items to move. /// The name of the new group. public static void CreateContentUpdateGroup(AddressableAssetSettings settings, List items, string groupName) { var contentGroup = settings.CreateGroup(settings.FindUniqueGroupName(groupName), false, false, true, null); var schema = contentGroup.AddSchema(); schema.BuildPath.SetVariableByName(settings, AddressableAssetSettings.kRemoteBuildPath); schema.LoadPath.SetVariableByName(settings, AddressableAssetSettings.kRemoteLoadPath); schema.BundleMode = BundledAssetGroupSchema.BundlePackingMode.PackTogether; contentGroup.AddSchema().StaticContent = false; settings.MoveEntries(items, contentGroup); } /// /// Functor to filter AddressableAssetGroups during content update. If the functor returns false, the group is excluded from the update. /// public static Func GroupFilterFunc = GroupFilter; internal static bool GroupFilter(AddressableAssetGroup g) { if (g == null) return false; if (!g.HasSchema() || !g.GetSchema().StaticContent) return false; if (!g.HasSchema() || !g.GetSchema().IncludeInBuild) return false; return true; } } }