using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using UnityEditor.AddressableAssets.Build.BuildPipelineTasks; using UnityEditor.AddressableAssets.Settings; using UnityEditor.AddressableAssets.Settings.GroupSchemas; using UnityEditor.Build.Pipeline; using UnityEditor.Build.Pipeline.Interfaces; using UnityEditor.Build.Pipeline.Tasks; using UnityEditor.Build.Pipeline.Utilities; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.AddressableAssets.Initialization; using UnityEngine.AddressableAssets.ResourceLocators; using UnityEngine.AddressableAssets.ResourceProviders; using UnityEngine.Build.Pipeline; using UnityEngine.ResourceManagement.ResourceProviders; using UnityEngine.ResourceManagement.Util; using static UnityEditor.AddressableAssets.Build.ContentUpdateScript; namespace UnityEditor.AddressableAssets.Build.DataBuilders { using Debug = UnityEngine.Debug; /// /// Build scripts used for player builds and running with bundles in the editor. /// [CreateAssetMenu(fileName = "BuildScriptPacked.asset", menuName = "Addressables/Content Builders/Default Build Script")] public class BuildScriptPackedMode : BuildScriptBase { /// public override string Name { get { return "Default Build Script"; } } internal List m_ResourceProviderData; List m_AllBundleInputDefs; List m_OutputAssetBundleNames; HashSet m_CreatedProviderIds; UnityEditor.Build.Pipeline.Utilities.LinkXmlGenerator m_Linker; Dictionary m_BundleToInternalId = new Dictionary(); private string m_CatalogBuildPath; internal List ResourceProviderData => m_ResourceProviderData.ToList(); /// public override bool CanBuildData() { return typeof(T).IsAssignableFrom(typeof(AddressablesPlayerBuildResult)); } /// protected override TResult BuildDataImplementation(AddressablesDataBuilderInput builderInput) { TResult result = default(TResult); var timer = new Stopwatch(); timer.Start(); InitializeBuildContext(builderInput, out AddressableAssetsBuildContext aaContext); using (m_Log.ScopedStep(LogLevel.Info, "ProcessAllGroups")) { var errorString = ProcessAllGroups(aaContext); if (!string.IsNullOrEmpty(errorString)) result = AddressableAssetBuildResult.CreateResult(null, 0, errorString); } if (result == null) { result = DoBuild(builderInput, aaContext); } if (result != null) result.Duration = timer.Elapsed.TotalSeconds; return result; } internal void InitializeBuildContext(AddressablesDataBuilderInput builderInput, out AddressableAssetsBuildContext aaContext) { var aaSettings = builderInput.AddressableSettings; m_AllBundleInputDefs = new List(); m_OutputAssetBundleNames = new List(); var bundleToAssetGroup = new Dictionary(); var runtimeData = new ResourceManagerRuntimeData { CertificateHandlerType = aaSettings.CertificateHandlerType, BuildTarget = builderInput.Target.ToString(), ProfileEvents = builderInput.ProfilerEventsEnabled, LogResourceManagerExceptions = aaSettings.buildSettings.LogResourceManagerExceptions, DisableCatalogUpdateOnStartup = aaSettings.DisableCatalogUpdateOnStartup, IsLocalCatalogInBundle = aaSettings.BundleLocalCatalog, #if UNITY_2019_3_OR_NEWER AddressablesVersion = PackageManager.PackageInfo.FindForAssembly(typeof(Addressables).Assembly)?.version, #endif MaxConcurrentWebRequests = aaSettings.MaxConcurrentWebRequests, CatalogRequestsTimeout = aaSettings.CatalogRequestsTimeout }; m_Linker = UnityEditor.Build.Pipeline.Utilities.LinkXmlGenerator.CreateDefault(); m_Linker.AddAssemblies(new[] { typeof(Addressables).Assembly, typeof(UnityEngine.ResourceManagement.ResourceManager).Assembly }); m_Linker.AddTypes(runtimeData.CertificateHandlerType); m_ResourceProviderData = new List(); aaContext = new AddressableAssetsBuildContext { Settings = aaSettings, runtimeData = runtimeData, bundleToAssetGroup = bundleToAssetGroup, locations = new List(), providerTypes = new HashSet(), assetEntries = new List() }; m_CreatedProviderIds = new HashSet(); } struct SBPSettingsOverwriterScope : IDisposable { bool m_PrevSlimResults; public SBPSettingsOverwriterScope(bool forceFullWriteResults) { m_PrevSlimResults = ScriptableBuildPipeline.slimWriteResults; if (forceFullWriteResults) ScriptableBuildPipeline.slimWriteResults = false; } public void Dispose() { ScriptableBuildPipeline.slimWriteResults = m_PrevSlimResults; } } internal static string GetBuiltInShaderBundleNamePrefix(AddressableAssetsBuildContext aaContext) { return GetBuiltInShaderBundleNamePrefix(aaContext.Settings); } internal static string GetBuiltInShaderBundleNamePrefix(AddressableAssetSettings settings) { string value = ""; switch (settings.ShaderBundleNaming) { case ShaderBundleNaming.DefaultGroupGuid: value = settings.DefaultGroup.Guid; break; case ShaderBundleNaming.ProjectName: value = Hash128.Compute(GetProjectName()).ToString(); break; case ShaderBundleNaming.Custom: value = settings.ShaderBundleCustomNaming; break; } return value; } void AddBundleProvider(BundledAssetGroupSchema schema) { var bundleProviderId = schema.GetBundleCachedProviderId(); if (!m_CreatedProviderIds.Contains(bundleProviderId)) { m_CreatedProviderIds.Add(bundleProviderId); var bundleProviderType = schema.AssetBundleProviderType.Value; var bundleProviderData = ObjectInitializationData.CreateSerializedInitializationData(bundleProviderType, bundleProviderId); m_ResourceProviderData.Add(bundleProviderData); } } internal static string GetMonoScriptBundleNamePrefix(AddressableAssetsBuildContext aaContext) { return GetMonoScriptBundleNamePrefix(aaContext.Settings); } internal static string GetMonoScriptBundleNamePrefix(AddressableAssetSettings settings) { string value = null; switch (settings.MonoScriptBundleNaming) { case MonoScriptBundleNaming.ProjectName: value = Hash128.Compute(GetProjectName()).ToString(); break; case MonoScriptBundleNaming.DefaultGroupGuid: value = settings.DefaultGroup.Guid; break; case MonoScriptBundleNaming.Custom: value = settings.MonoScriptBundleCustomNaming; break; } return value; } /// /// The method that does the actual building after all the groups have been processed. /// /// The generic builderInput of the /// /// /// protected virtual TResult DoBuild(AddressablesDataBuilderInput builderInput, AddressableAssetsBuildContext aaContext) where TResult : IDataBuilderResult { ExtractDataTask extractData = new ExtractDataTask(); List carryOverCachedState = new List(); var tempPath = Path.GetDirectoryName(Application.dataPath) + "/" + Addressables.LibraryPath + PlatformMappingService.GetPlatformPathSubFolder() + "/addressables_content_state.bin"; var playerBuildVersion = builderInput.PlayerVersion; if (m_AllBundleInputDefs.Count > 0) { if (!BuildUtility.CheckModifiedScenesAndAskToSave()) return AddressableAssetBuildResult.CreateResult(null, 0, "Unsaved scenes"); var buildTarget = builderInput.Target; var buildTargetGroup = builderInput.TargetGroup; var buildParams = new AddressableAssetsBundleBuildParameters( aaContext.Settings, aaContext.bundleToAssetGroup, buildTarget, buildTargetGroup, aaContext.Settings.buildSettings.bundleBuildPath); var builtinShaderBundleName = GetBuiltInShaderBundleNamePrefix(aaContext) + "_unitybuiltinshaders.bundle"; var schema = aaContext.Settings.DefaultGroup.GetSchema(); AddBundleProvider(schema); string monoScriptBundleName = GetMonoScriptBundleNamePrefix(aaContext); if (!string.IsNullOrEmpty(monoScriptBundleName)) monoScriptBundleName += "_monoscripts.bundle"; var buildTasks = RuntimeDataBuildTasks(builtinShaderBundleName, monoScriptBundleName); buildTasks.Add(extractData); IBundleBuildResults results; using (m_Log.ScopedStep(LogLevel.Info, "ContentPipeline.BuildAssetBundles")) using (new SBPSettingsOverwriterScope(ProjectConfigData.GenerateBuildLayout)) // build layout generation requires full SBP write results { var exitCode = ContentPipeline.BuildAssetBundles(buildParams, new BundleBuildContent(m_AllBundleInputDefs), out results, buildTasks, aaContext, m_Log); if (exitCode < ReturnCode.Success) return AddressableAssetBuildResult.CreateResult(null, 0, "SBP Error" + exitCode); } var groups = aaContext.Settings.groups.Where(g => g != null); var bundleRenameMap = new Dictionary(); var postCatalogUpdateCallbacks = new List(); using (m_Log.ScopedStep(LogLevel.Info, "PostProcessBundles")) using (var progressTracker = new UnityEditor.Build.Pipeline.Utilities.ProgressTracker()) { progressTracker.UpdateTask("Post Processing AssetBundles"); Dictionary primaryKeyToCatalogEntry = new Dictionary(); foreach (var loc in aaContext.locations) if (loc != null && loc.Keys[0] != null && loc.Keys[0] is string && !primaryKeyToCatalogEntry.ContainsKey((string)loc.Keys[0])) primaryKeyToCatalogEntry[(string)loc.Keys[0]] = loc; foreach (var assetGroup in groups) { if (aaContext.assetGroupToBundles.TryGetValue(assetGroup, out List buildBundles)) { using (m_Log.ScopedStep(LogLevel.Info, assetGroup.name)) { List outputBundles = new List(); for (int i = 0; i < buildBundles.Count; ++i) { var b = m_AllBundleInputDefs.FindIndex(inputDef => buildBundles[i].StartsWith(inputDef.assetBundleName)); outputBundles.Add(b >= 0 ? m_OutputAssetBundleNames[b] : buildBundles[i]); } PostProcessBundles(assetGroup, buildBundles, outputBundles, results, aaContext.runtimeData, aaContext.locations, builderInput.Registry, primaryKeyToCatalogEntry, bundleRenameMap, postCatalogUpdateCallbacks); } } } } using (m_Log.ScopedStep(LogLevel.Info, "Process Catalog Entries")) { ProcessCatalogEntriesForBuild(aaContext, groups, builderInput, extractData.WriteData, carryOverCachedState, m_BundleToInternalId); foreach (var postUpdateCatalogCallback in postCatalogUpdateCallbacks) postUpdateCatalogCallback.Invoke(); foreach (var r in results.WriteResults) { var resultValue = r.Value; m_Linker.AddTypes(resultValue.includedTypes); #if UNITY_2021_1_OR_NEWER m_Linker.AddSerializedClass(resultValue.includedSerializeReferenceFQN); #else if (resultValue.GetType().GetProperty("includedSerializeReferenceFQN") != null) m_Linker.AddSerializedClass(resultValue.GetType().GetProperty("includedSerializeReferenceFQN").GetValue(resultValue) as System.Collections.Generic.IEnumerable); #endif } } using (m_Log.ScopedStep(LogLevel.Info, "Generate Build Layout")) { if (ProjectConfigData.GenerateBuildLayout) { using (var progressTracker = new UnityEditor.Build.Pipeline.Utilities.ProgressTracker()) { progressTracker.UpdateTask("Generating Build Layout"); List tasks = new List(); var buildLayoutTask = new BuildLayoutGenerationTask(); buildLayoutTask.m_BundleNameRemap = bundleRenameMap; tasks.Add(buildLayoutTask); BuildTasksRunner.Run(tasks, extractData.m_BuildContext); } } } } ContentCatalogData contentCatalog; using (m_Log.ScopedStep(LogLevel.Info, "Generate Catalog")) { contentCatalog = new ContentCatalogData(ResourceManagerRuntimeData.kCatalogAddress); contentCatalog.SetData(aaContext.locations.OrderBy(f => f.InternalId).ToList(), aaContext.Settings.OptimizeCatalogSize); contentCatalog.ResourceProviderData.AddRange(m_ResourceProviderData); foreach (var t in aaContext.providerTypes) contentCatalog.ResourceProviderData.Add(ObjectInitializationData.CreateSerializedInitializationData(t)); contentCatalog.InstanceProviderData = ObjectInitializationData.CreateSerializedInitializationData(instanceProviderType.Value); contentCatalog.SceneProviderData = ObjectInitializationData.CreateSerializedInitializationData(sceneProviderType.Value); //save catalog var jsonText = JsonUtility.ToJson(contentCatalog); CreateCatalogFiles(jsonText, builderInput, aaContext); } using (m_Log.ScopedStep(LogLevel.Info, "Generate link")) { foreach (var pd in contentCatalog.ResourceProviderData) { m_Linker.AddTypes(pd.ObjectType.Value); m_Linker.AddTypes(pd.GetRuntimeTypes()); } m_Linker.AddTypes(contentCatalog.InstanceProviderData.ObjectType.Value); m_Linker.AddTypes(contentCatalog.InstanceProviderData.GetRuntimeTypes()); m_Linker.AddTypes(contentCatalog.SceneProviderData.ObjectType.Value); m_Linker.AddTypes(contentCatalog.SceneProviderData.GetRuntimeTypes()); foreach (var io in aaContext.Settings.InitializationObjects) { var provider = io as IObjectInitializationDataProvider; if (provider != null) { var id = provider.CreateObjectInitializationData(); aaContext.runtimeData.InitializationObjects.Add(id); m_Linker.AddTypes(id.ObjectType.Value); m_Linker.AddTypes(id.GetRuntimeTypes()); } } m_Linker.AddTypes(typeof(Addressables)); Directory.CreateDirectory(Addressables.BuildPath + "/AddressablesLink/"); m_Linker.Save(Addressables.BuildPath + "/AddressablesLink/link.xml"); } var settingsPath = Addressables.BuildPath + "/" + builderInput.RuntimeSettingsFilename; using (m_Log.ScopedStep(LogLevel.Info, "Generate Settings")) WriteFile(settingsPath, JsonUtility.ToJson(aaContext.runtimeData), builderInput.Registry); if (extractData.BuildCache != null && builderInput.PreviousContentState == null) { using (m_Log.ScopedStep(LogLevel.Info, "Generate Content Update State")) { var remoteCatalogLoadPath = aaContext.Settings.BuildRemoteCatalog ? aaContext.Settings.RemoteCatalogLoadPath.GetValue(aaContext.Settings) : string.Empty; var allEntries = new List(); using (m_Log.ScopedStep(LogLevel.Info, "Get Assets")) aaContext.Settings.GetAllAssets(allEntries, false, ContentUpdateScript.GroupFilter); if (ContentUpdateScript.SaveContentState(aaContext.locations, tempPath, allEntries, extractData.DependencyData, playerBuildVersion, remoteCatalogLoadPath, carryOverCachedState)) { string contentStatePath = ContentUpdateScript.GetContentStateDataPath(false); try { File.Copy(tempPath, contentStatePath, true); builderInput.Registry.AddFile(contentStatePath); } catch (UnauthorizedAccessException uae) { if (!AddressableAssetUtility.IsVCAssetOpenForEdit(contentStatePath)) Debug.LogErrorFormat("Cannot access the file {0}. It may be locked by version control.", contentStatePath); else Debug.LogException(uae); } catch (Exception e) { Debug.LogException(e); } } } } return AddressableAssetBuildResult.CreateResult(settingsPath, aaContext.locations.Count); } private static void ProcessCatalogEntriesForBuild(AddressableAssetsBuildContext aaContext, IEnumerable validGroups, AddressablesDataBuilderInput builderInput, IBundleWriteData writeData, List carryOverCachedState, Dictionary bundleToInternalId) { using (var progressTracker = new UnityEditor.Build.Pipeline.Utilities.ProgressTracker()) { progressTracker.UpdateTask("Post Processing Catalog Entries"); Dictionary locationIdToCatalogEntryMap = BuildLocationIdToCatalogEntryMap(aaContext.locations); if (builderInput.PreviousContentState != null) { ContentUpdateContext contentUpdateContext = new ContentUpdateContext() { BundleToInternalBundleIdMap = bundleToInternalId, GuidToPreviousAssetStateMap = BuildGuidToCachedAssetStateMap(builderInput.PreviousContentState, aaContext.Settings), IdToCatalogDataEntryMap = locationIdToCatalogEntryMap, WriteData = writeData, ContentState = builderInput.PreviousContentState, Registry = builderInput.Registry, PreviousAssetStateCarryOver = carryOverCachedState }; RevertUnchangedAssetsToPreviousAssetState.Run(aaContext, contentUpdateContext); } else { foreach (var assetGroup in validGroups) SetAssetEntriesBundleFileIdToCatalogEntryBundleFileId(assetGroup.entries, bundleToInternalId, writeData, locationIdToCatalogEntryMap); } } bundleToInternalId.Clear(); } private static Dictionary BuildLocationIdToCatalogEntryMap(List locations) { Dictionary locationIdToCatalogEntryMap = new Dictionary(); foreach (var location in locations) locationIdToCatalogEntryMap[location.InternalId] = location; return locationIdToCatalogEntryMap; } private static Dictionary BuildGuidToCachedAssetStateMap(AddressablesContentState contentState, AddressableAssetSettings settings) { Dictionary addressableEntryToCachedStateMap = new Dictionary(); foreach (var cachedInfo in contentState.cachedInfos) addressableEntryToCachedStateMap[cachedInfo.asset.guid.ToString()] = cachedInfo; return addressableEntryToCachedStateMap; } internal bool CreateCatalogFiles(string jsonText, AddressablesDataBuilderInput builderInput, AddressableAssetsBuildContext aaContext) { if (string.IsNullOrEmpty(jsonText) || builderInput == null || aaContext == null) { Addressables.LogError("Unable to create content catalog (Null arguments)."); return false; } // Path needs to be resolved at runtime. string localLoadPath = "{UnityEngine.AddressableAssets.Addressables.RuntimePath}/" + builderInput.RuntimeCatalogFilename; m_CatalogBuildPath = Path.Combine(Addressables.BuildPath, builderInput.RuntimeCatalogFilename); if (aaContext.Settings.BundleLocalCatalog) { localLoadPath = localLoadPath.Replace(".json", ".bundle"); m_CatalogBuildPath = m_CatalogBuildPath.Replace(".json", ".bundle"); var returnCode = CreateCatalogBundle(m_CatalogBuildPath, jsonText, builderInput); if (returnCode != ReturnCode.Success || !File.Exists(m_CatalogBuildPath)) { Addressables.LogError($"An error occured during the creation of the content catalog bundle (return code {returnCode})."); return false; } } else { WriteFile(m_CatalogBuildPath, jsonText, builderInput.Registry); } string[] dependencyHashes = null; if (aaContext.Settings.BuildRemoteCatalog) { dependencyHashes = CreateRemoteCatalog(jsonText, aaContext.runtimeData.CatalogLocations, aaContext.Settings, builderInput, new ProviderLoadRequestOptions() {IgnoreFailures = true}); } aaContext.runtimeData.CatalogLocations.Add(new ResourceLocationData( new[] { ResourceManagerRuntimeData.kCatalogAddress }, localLoadPath, typeof(ContentCatalogProvider), typeof(ContentCatalogData), dependencyHashes)); return true; } internal static string GetProjectName() { return new DirectoryInfo(Path.GetDirectoryName(Application.dataPath)).Name; } internal ReturnCode CreateCatalogBundle(string filepath, string jsonText, AddressablesDataBuilderInput builderInput) { if (string.IsNullOrEmpty(filepath) || string.IsNullOrEmpty(jsonText) || builderInput == null) { throw new ArgumentException("Unable to create catalog bundle (null arguments)."); } // A bundle requires an actual asset var tempFolderName = "TempCatalogFolder"; var configFolder = AddressableAssetSettingsDefaultObject.kDefaultConfigFolder; if (builderInput.AddressableSettings != null && builderInput.AddressableSettings.IsPersisted) configFolder = builderInput.AddressableSettings.ConfigFolder; var tempFolderPath = Path.Combine(configFolder, tempFolderName); var tempFilePath = Path.Combine(tempFolderPath, Path.GetFileName(filepath).Replace(".bundle", ".json")); if (!WriteFile(tempFilePath, jsonText, builderInput.Registry)) { throw new Exception("An error occured during the creation of temporary files needed to bundle the content catalog."); } AssetDatabase.Refresh(); var bundleBuildContent = new BundleBuildContent(new[] { new AssetBundleBuild() { assetBundleName = Path.GetFileName(filepath), assetNames = new[] {tempFilePath}, addressableNames = new string[0] } }); var buildTasks = new List { new CalculateAssetDependencyData(), new GenerateBundlePacking(), new GenerateBundleCommands(), new WriteSerializedFiles(), new ArchiveAndCompressBundles() }; var buildParams = new BundleBuildParameters(builderInput.Target, builderInput.TargetGroup, Path.GetDirectoryName(filepath)); if (builderInput.Target == BuildTarget.WebGL) buildParams.BundleCompression = BuildCompression.LZ4Runtime; var retCode = ContentPipeline.BuildAssetBundles(buildParams, bundleBuildContent, out IBundleBuildResults result, buildTasks, m_Log); if (Directory.Exists(tempFolderPath)) { Directory.Delete(tempFolderPath, true); builderInput.Registry.RemoveFile(tempFilePath); } var tempFolderMetaFile = tempFolderPath + ".meta"; if (File.Exists(tempFolderMetaFile)) { File.Delete(tempFolderMetaFile); builderInput.Registry.RemoveFile(tempFolderMetaFile); } if (File.Exists(filepath)) { builderInput.Registry.AddFile(filepath); } return retCode; } internal static void SetAssetEntriesBundleFileIdToCatalogEntryBundleFileId(ICollection assetEntries, Dictionary bundleNameToInternalBundleIdMap, IBundleWriteData writeData, Dictionary locationIdToCatalogEntryMap) { foreach (var loc in assetEntries) { AddressableAssetEntry processedEntry = loc; if (loc.IsFolder && loc.SubAssets.Count > 0) processedEntry = loc.SubAssets[0]; GUID guid = new GUID(processedEntry.guid); //For every entry in the write data we need to ensure the BundleFileId is set so we can save it correctly in the cached state if (writeData.AssetToFiles.TryGetValue(guid, out List files)) { string file = files[0]; string fullBundleName = writeData.FileToBundle[file]; string convertedLocation; if (!bundleNameToInternalBundleIdMap.TryGetValue(fullBundleName, out convertedLocation)) { Debug.LogException(new Exception($"Unable to find bundleId for key: {fullBundleName}.")); } if (locationIdToCatalogEntryMap.TryGetValue(convertedLocation, out ContentCatalogDataEntry catalogEntry)) { loc.BundleFileId = catalogEntry.InternalId; //This is where we strip out the temporary hash added to the bundle name for Content Update for the AssetEntry if (loc.parentGroup?.GetSchema()?.BundleNaming == BundledAssetGroupSchema.BundleNamingStyle.NoHash) { loc.BundleFileId = StripHashFromBundleLocation(loc.BundleFileId); } } } } } static string StripHashFromBundleLocation(string hashedBundleLocation) { return hashedBundleLocation.Remove(hashedBundleLocation.LastIndexOf("_")) + ".bundle"; } /// protected override string ProcessGroup(AddressableAssetGroup assetGroup, AddressableAssetsBuildContext aaContext) { 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); if (!string.IsNullOrEmpty(errorString)) return errorString; } return string.Empty; } /// /// Called per group per schema to evaluate that schema. This can be an easy entry point for implementing the /// build aspects surrounding a custom schema. Note, you should not rely on schemas getting called in a specific /// order. /// /// The schema to process /// The group this schema was pulled from /// The general Addressables build builderInput /// protected virtual string ProcessGroupSchema(AddressableAssetGroupSchema schema, AddressableAssetGroup assetGroup, AddressableAssetsBuildContext aaContext) { var playerDataSchema = schema as PlayerDataGroupSchema; if (playerDataSchema != null) return ProcessPlayerDataSchema(playerDataSchema, assetGroup, aaContext); var bundledAssetSchema = schema as BundledAssetGroupSchema; if (bundledAssetSchema != null) return ProcessBundledAssetSchema(bundledAssetSchema, assetGroup, aaContext); return string.Empty; } internal string ProcessPlayerDataSchema( PlayerDataGroupSchema schema, AddressableAssetGroup assetGroup, AddressableAssetsBuildContext aaContext) { if (CreateLocationsForPlayerData(schema, assetGroup, aaContext.locations, aaContext.providerTypes)) { if (!m_CreatedProviderIds.Contains(typeof(LegacyResourcesProvider).Name)) { m_CreatedProviderIds.Add(typeof(LegacyResourcesProvider).Name); m_ResourceProviderData.Add(ObjectInitializationData.CreateSerializedInitializationData(typeof(LegacyResourcesProvider))); } } return string.Empty; } /// /// The processing of the bundled asset schema. This is where the bundle(s) for a given group are actually setup. /// /// The BundledAssetGroupSchema to process /// The group this schema was pulled from /// The general Addressables build builderInput /// The error string, if any. protected virtual string ProcessBundledAssetSchema( BundledAssetGroupSchema schema, AddressableAssetGroup assetGroup, AddressableAssetsBuildContext aaContext) { if (schema == null || !schema.IncludeInBuild || !assetGroup.entries.Any()) return string.Empty; var errorStr = ErrorCheckBundleSettings(schema, assetGroup, aaContext.Settings); if (!string.IsNullOrEmpty(errorStr)) return errorStr; AddBundleProvider(schema); var assetProviderId = schema.GetAssetCachedProviderId(); if (!m_CreatedProviderIds.Contains(assetProviderId)) { m_CreatedProviderIds.Add(assetProviderId); var assetProviderType = schema.BundledAssetProviderType.Value; var assetProviderData = ObjectInitializationData.CreateSerializedInitializationData(assetProviderType, assetProviderId); m_ResourceProviderData.Add(assetProviderData); } #if UNITY_2022_1_OR_NEWER string loadPath = schema.LoadPath.GetValue(aaContext.Settings); if (loadPath.StartsWith("http://") && PlayerSettings.insecureHttpOption == InsecureHttpOption.NotAllowed) Addressables.LogWarning($"Addressable group {assetGroup.Name} uses insecure http for its load path. To allow http connections for UnityWebRequests, change your settings in Edit > Project Settings > Player > Other Settings > Configuration > Allow downloads over HTTP."); #endif if (schema.Compression == BundledAssetGroupSchema.BundleCompressionMode.LZMA && aaContext.runtimeData.BuildTarget == BuildTarget.WebGL.ToString()) Addressables.LogWarning($"Addressable group {assetGroup.Name} uses LZMA compression, which cannot be decompressed on WebGL. Use LZ4 compression instead."); var bundleInputDefs = new List(); var list = PrepGroupBundlePacking(assetGroup, bundleInputDefs, schema); aaContext.assetEntries.AddRange(list); List uniqueNames = HandleDuplicateBundleNames(bundleInputDefs, aaContext.bundleToAssetGroup, assetGroup.Guid); m_OutputAssetBundleNames.AddRange(uniqueNames); m_AllBundleInputDefs.AddRange(bundleInputDefs); return string.Empty; } internal static List HandleDuplicateBundleNames(List bundleInputDefs, Dictionary bundleToAssetGroup = null, string assetGroupGuid = null) { var generatedUniqueNames = new List(); var handledNames = new HashSet(); for (int i = 0; i < bundleInputDefs.Count; i++) { AssetBundleBuild bundleBuild = bundleInputDefs[i]; string assetBundleName = bundleBuild.assetBundleName; if (handledNames.Contains(assetBundleName)) { int count = 1; var newName = assetBundleName; while (handledNames.Contains(newName) && count < 1000) newName = assetBundleName.Replace(".bundle", string.Format("{0}.bundle", count++)); assetBundleName = newName; } string hashedAssetBundleName = HashingMethods.Calculate(assetBundleName) + ".bundle"; generatedUniqueNames.Add(assetBundleName); handledNames.Add(assetBundleName); bundleBuild.assetBundleName = hashedAssetBundleName; bundleInputDefs[i] = bundleBuild; if (bundleToAssetGroup != null) bundleToAssetGroup.Add(hashedAssetBundleName, assetGroupGuid); } return generatedUniqueNames; } internal static string ErrorCheckBundleSettings(BundledAssetGroupSchema schema, AddressableAssetGroup assetGroup, AddressableAssetSettings settings) { var message = string.Empty; string buildPath = settings.profileSettings.GetValueById(settings.activeProfileId, schema.BuildPath.Id); string loadPath = settings.profileSettings.GetValueById(settings.activeProfileId, schema.LoadPath.Id); bool buildLocal = buildPath.Contains("[UnityEngine.AddressableAssets.Addressables.BuildPath]"); bool loadLocal = loadPath.Contains("{UnityEngine.AddressableAssets.Addressables.RuntimePath}"); if (buildLocal && !loadLocal) { message = "BuildPath for group '" + assetGroup.Name + "' is set to the dynamic-lookup version of StreamingAssets, but LoadPath is not. \n"; } else if (!buildLocal && loadLocal) { message = "LoadPath for group " + assetGroup.Name + " is set to the dynamic-lookup version of StreamingAssets, but BuildPath is not. These paths must both use the dynamic-lookup, or both not use it. \n"; } if (!string.IsNullOrEmpty(message)) { message += "BuildPath: '" + buildPath + "'\n"; message += "LoadPath: '" + loadPath + "'"; } if (schema.Compression == BundledAssetGroupSchema.BundleCompressionMode.LZMA && (buildLocal || loadLocal)) { Debug.LogWarningFormat("Bundle compression is set to LZMA, but group {0} uses local content.", assetGroup.Name); } return message; } internal static string CalculateGroupHash(BundledAssetGroupSchema.BundleInternalIdMode mode, AddressableAssetGroup assetGroup, IEnumerable entries) { switch (mode) { case BundledAssetGroupSchema.BundleInternalIdMode.GroupGuid: return assetGroup.Guid; case BundledAssetGroupSchema.BundleInternalIdMode.GroupGuidProjectIdHash: return HashingMethods.Calculate(assetGroup.Guid, Application.cloudProjectId).ToString(); case BundledAssetGroupSchema.BundleInternalIdMode.GroupGuidProjectIdEntriesHash: return HashingMethods.Calculate(assetGroup.Guid, Application.cloudProjectId, new HashSet(entries.Select(e => e.guid))).ToString(); } throw new Exception("Invalid naming mode."); } /// /// Processes an AddressableAssetGroup and generates AssetBundle input definitions based on the BundlePackingMode. /// /// The AddressableAssetGroup to be processed. /// The list of bundle definitions fed into the build pipeline AssetBundleBuild /// The BundledAssetGroupSchema of used to process the assetGroup. /// A filter to remove AddressableAssetEntries from being processed in the build. /// The total list of AddressableAssetEntries that were processed. public static List PrepGroupBundlePacking(AddressableAssetGroup assetGroup, List bundleInputDefs, BundledAssetGroupSchema schema, Func entryFilter = null) { var combinedEntries = new List(); var packingMode = schema.BundleMode; var namingMode = schema.InternalBundleIdMode; bool ignoreUnsupportedFilesInBuild = assetGroup.Settings.IgnoreUnsupportedFilesInBuild; switch (packingMode) { case BundledAssetGroupSchema.BundlePackingMode.PackTogether: { var allEntries = new List(); foreach (AddressableAssetEntry a in assetGroup.entries) { if (entryFilter != null && !entryFilter(a)) continue; a.GatherAllAssets(allEntries, true, true, false, entryFilter); } combinedEntries.AddRange(allEntries); GenerateBuildInputDefinitions(allEntries, bundleInputDefs, CalculateGroupHash(namingMode, assetGroup, allEntries), "all", ignoreUnsupportedFilesInBuild); } break; case BundledAssetGroupSchema.BundlePackingMode.PackSeparately: { foreach (AddressableAssetEntry a in assetGroup.entries) { if (entryFilter != null && !entryFilter(a)) continue; var allEntries = new List(); a.GatherAllAssets(allEntries, true, true, false, entryFilter); combinedEntries.AddRange(allEntries); GenerateBuildInputDefinitions(allEntries, bundleInputDefs, CalculateGroupHash(namingMode, assetGroup, allEntries), a.address, ignoreUnsupportedFilesInBuild); } } break; case BundledAssetGroupSchema.BundlePackingMode.PackTogetherByLabel: { var labelTable = new Dictionary>(); foreach (AddressableAssetEntry a in assetGroup.entries) { if (entryFilter != null && !entryFilter(a)) continue; var sb = new StringBuilder(); foreach (var l in a.labels) sb.Append(l); var key = sb.ToString(); List entries; if (!labelTable.TryGetValue(key, out entries)) labelTable.Add(key, entries = new List()); entries.Add(a); } foreach (var entryGroup in labelTable) { var allEntries = new List(); foreach (var a in entryGroup.Value) { if (entryFilter != null && !entryFilter(a)) continue; a.GatherAllAssets(allEntries, true, true, false, entryFilter); } combinedEntries.AddRange(allEntries); GenerateBuildInputDefinitions(allEntries, bundleInputDefs, CalculateGroupHash(namingMode, assetGroup, allEntries), entryGroup.Key, ignoreUnsupportedFilesInBuild); } } break; default: throw new Exception("Unknown Packing Mode"); } return combinedEntries; } internal static void GenerateBuildInputDefinitions(List allEntries, List buildInputDefs, string groupGuid, string address, bool ignoreUnsupportedFilesInBuild) { var scenes = new List(); var assets = new List(); foreach (var e in allEntries) { ThrowExceptionIfInvalidFiletypeOrAddress(e, ignoreUnsupportedFilesInBuild); if (string.IsNullOrEmpty(e.AssetPath)) continue; if (e.IsScene) scenes.Add(e); else assets.Add(e); } if (assets.Count > 0) buildInputDefs.Add(GenerateBuildInputDefinition(assets, groupGuid + "_assets_" + address + ".bundle")); if (scenes.Count > 0) buildInputDefs.Add(GenerateBuildInputDefinition(scenes, groupGuid + "_scenes_" + address + ".bundle")); } private static void ThrowExceptionIfInvalidFiletypeOrAddress(AddressableAssetEntry entry, bool ignoreUnsupportedFilesInBuild) { if (entry.guid.Length > 0 && entry.address.Contains("[") && entry.address.Contains("]")) throw new Exception($"Address '{entry.address}' cannot contain '[ ]'."); if (entry.MainAssetType == typeof(DefaultAsset) && !AssetDatabase.IsValidFolder(entry.AssetPath)) { if (ignoreUnsupportedFilesInBuild) Debug.LogWarning($"Cannot recognize file type for entry located at '{entry.AssetPath}'. Asset location will be ignored."); else throw new Exception($"Cannot recognize file type for entry located at '{entry.AssetPath}'. Asset import failed for using an unsupported file type."); } } internal static AssetBundleBuild GenerateBuildInputDefinition(List assets, string name) { var assetInternalIds = new HashSet(); var assetsInputDef = new AssetBundleBuild(); assetsInputDef.assetBundleName = name.ToLower().Replace(" ", "").Replace('\\', '/').Replace("//", "/"); assetsInputDef.assetNames = assets.Select(s => s.AssetPath).ToArray(); assetsInputDef.addressableNames = assets.Select(s => s.GetAssetLoadPath(true, assetInternalIds)).ToArray(); return assetsInputDef; } static string[] CreateRemoteCatalog(string jsonText, List locations, AddressableAssetSettings aaSettings, AddressablesDataBuilderInput builderInput, ProviderLoadRequestOptions catalogLoadOptions) { string[] dependencyHashes = null; var contentHash = HashingMethods.Calculate(jsonText).ToString(); var versionedFileName = aaSettings.profileSettings.EvaluateString(aaSettings.activeProfileId, "/catalog_" + builderInput.PlayerVersion); var remoteBuildFolder = aaSettings.RemoteCatalogBuildPath.GetValue(aaSettings); var remoteLoadFolder = aaSettings.RemoteCatalogLoadPath.GetValue(aaSettings); if (string.IsNullOrEmpty(remoteBuildFolder) || string.IsNullOrEmpty(remoteLoadFolder) || remoteBuildFolder == AddressableAssetProfileSettings.undefinedEntryValue || remoteLoadFolder == AddressableAssetProfileSettings.undefinedEntryValue) { Addressables.LogWarning("Remote Build and/or Load paths are not set on the main AddressableAssetSettings asset, but 'Build Remote Catalog' is true. Cannot create remote catalog. In the inspector for any group, double click the 'Addressable Asset Settings' object to begin inspecting it. '" + remoteBuildFolder + "', '" + remoteLoadFolder + "'"); } else { var remoteJsonBuildPath = remoteBuildFolder + versionedFileName + ".json"; var remoteHashBuildPath = remoteBuildFolder + versionedFileName + ".hash"; WriteFile(remoteJsonBuildPath, jsonText, builderInput.Registry); WriteFile(remoteHashBuildPath, contentHash, builderInput.Registry); dependencyHashes = new string[((int)ContentCatalogProvider.DependencyHashIndex.Count)]; dependencyHashes[(int)ContentCatalogProvider.DependencyHashIndex.Remote] = ResourceManagerRuntimeData.kCatalogAddress + "RemoteHash"; dependencyHashes[(int)ContentCatalogProvider.DependencyHashIndex.Cache] = ResourceManagerRuntimeData.kCatalogAddress + "CacheHash"; var remoteHashLoadPath = remoteLoadFolder + versionedFileName + ".hash"; var remoteHashLoadLocation = new ResourceLocationData( new[] {dependencyHashes[(int)ContentCatalogProvider.DependencyHashIndex.Remote]}, remoteHashLoadPath, typeof(TextDataProvider), typeof(string)); remoteHashLoadLocation.Data = catalogLoadOptions.Copy(); locations.Add(remoteHashLoadLocation); var cacheLoadPath = "{UnityEngine.Application.persistentDataPath}/com.unity.addressables" + versionedFileName + ".hash"; var cacheLoadLocation = new ResourceLocationData( new[] {dependencyHashes[(int)ContentCatalogProvider.DependencyHashIndex.Cache]}, cacheLoadPath, typeof(TextDataProvider), typeof(string)); cacheLoadLocation.Data = catalogLoadOptions.Copy(); locations.Add(cacheLoadLocation); } return dependencyHashes; } // Tests can set this flag to prevent player script compilation. This is the most expensive part of small builds // and isn't needed for most tests. internal static bool s_SkipCompilePlayerScripts = false; static IList RuntimeDataBuildTasks(string builtinShaderBundleName, string monoScriptBundleName) { var buildTasks = new List(); // Setup buildTasks.Add(new SwitchToBuildPlatform()); buildTasks.Add(new RebuildSpriteAtlasCache()); // Player Scripts if (!s_SkipCompilePlayerScripts) buildTasks.Add(new BuildPlayerScripts()); buildTasks.Add(new PostScriptsCallback()); // Dependency buildTasks.Add(new CalculateSceneDependencyData()); buildTasks.Add(new CalculateAssetDependencyData()); buildTasks.Add(new AddHashToBundleNameTask()); buildTasks.Add(new StripUnusedSpriteSources()); buildTasks.Add(new CreateBuiltInShadersBundle(builtinShaderBundleName)); if (!string.IsNullOrEmpty(monoScriptBundleName)) buildTasks.Add(new CreateMonoScriptBundle(monoScriptBundleName)); buildTasks.Add(new PostDependencyCallback()); // Packing buildTasks.Add(new GenerateBundlePacking()); buildTasks.Add(new UpdateBundleObjectLayout()); buildTasks.Add(new GenerateBundleCommands()); buildTasks.Add(new GenerateSubAssetPathMaps()); buildTasks.Add(new GenerateBundleMaps()); buildTasks.Add(new PostPackingCallback()); // Writing buildTasks.Add(new WriteSerializedFiles()); buildTasks.Add(new ArchiveAndCompressBundles()); buildTasks.Add(new GenerateLocationListsTask()); buildTasks.Add(new PostWritingCallback()); return buildTasks; } static void MoveFileToDestinationWithTimestampIfDifferent(string srcPath, string destPath, IBuildLogger log) { if (srcPath == destPath) return; DateTime time = File.GetLastWriteTime(srcPath); DateTime destTime = File.Exists(destPath) ? File.GetLastWriteTime(destPath) : new DateTime(); if (destTime == time) return; using (log.ScopedStep(LogLevel.Verbose, "Move File", $"{srcPath} -> {destPath}")) { var directory = Path.GetDirectoryName(destPath); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) Directory.CreateDirectory(directory); else if (File.Exists(destPath)) File.Delete(destPath); File.Move(srcPath, destPath); } } void PostProcessBundles(AddressableAssetGroup assetGroup, List buildBundles, List outputBundles, IBundleBuildResults buildResult, ResourceManagerRuntimeData runtimeData, List locations, FileRegistry registry, Dictionary primaryKeyToCatalogEntry, Dictionary bundleRenameMap, List postCatalogUpdateCallbacks) { var schema = assetGroup.GetSchema(); if (schema == null) return; var path = schema.BuildPath.GetValue(assetGroup.Settings); if (string.IsNullOrEmpty(path)) return; for (int i = 0; i < buildBundles.Count; ++i) { if (primaryKeyToCatalogEntry.TryGetValue(buildBundles[i], out ContentCatalogDataEntry dataEntry)) { var info = buildResult.BundleInfos[buildBundles[i]]; var requestOptions = new AssetBundleRequestOptions { Crc = schema.UseAssetBundleCrc ? info.Crc : 0, UseCrcForCachedBundle = schema.UseAssetBundleCrcForCachedBundles, UseUnityWebRequestForLocalBundles = schema.UseUnityWebRequestForLocalBundles, Hash = schema.UseAssetBundleCache ? info.Hash.ToString() : "", ChunkedTransfer = schema.ChunkedTransfer, RedirectLimit = schema.RedirectLimit, RetryCount = schema.RetryCount, Timeout = schema.Timeout, BundleName = Path.GetFileNameWithoutExtension(info.FileName), AssetLoadMode = schema.AssetLoadMode, BundleSize = GetFileSize(info.FileName), ClearOtherCachedVersionsWhenLoaded = schema.AssetBundledCacheClearBehavior == BundledAssetGroupSchema.CacheClearBehavior.ClearWhenWhenNewVersionLoaded }; dataEntry.Data = requestOptions; if (assetGroup == assetGroup.Settings.DefaultGroup && info.Dependencies.Length == 0 && !string.IsNullOrEmpty(info.FileName) && (info.FileName.EndsWith("_unitybuiltinshaders.bundle") || info.FileName.EndsWith("_monoscripts.bundle"))) { outputBundles[i] = ConstructAssetBundleName(null, schema, info, outputBundles[i]); } else { int extensionLength = Path.GetExtension(outputBundles[i]).Length; string[] deconstructedBundleName = outputBundles[i].Substring(0, outputBundles[i].Length - extensionLength).Split('_'); string reconstructedBundleName = string.Join("_", deconstructedBundleName, 1, deconstructedBundleName.Length - 1) + ".bundle"; outputBundles[i] = ConstructAssetBundleName(assetGroup, schema, info, reconstructedBundleName); } dataEntry.InternalId = dataEntry.InternalId.Remove(dataEntry.InternalId.Length - buildBundles[i].Length) + outputBundles[i]; dataEntry.Keys[0] = outputBundles[i]; ReplaceDependencyKeys(buildBundles[i], outputBundles[i], locations); if (!m_BundleToInternalId.ContainsKey(buildBundles[i])) m_BundleToInternalId.Add(buildBundles[i], dataEntry.InternalId); if (dataEntry.InternalId.StartsWith("http:\\")) dataEntry.InternalId = dataEntry.InternalId.Replace("http:\\", "http://").Replace("\\", "/"); if (dataEntry.InternalId.StartsWith("https:\\")) dataEntry.InternalId = dataEntry.InternalId.Replace("https:\\", "https://").Replace("\\", "/"); } else { Debug.LogWarningFormat("Unable to find ContentCatalogDataEntry for bundle {0}.", outputBundles[i]); } var targetPath = Path.Combine(path, outputBundles[i]); var srcPath = Path.Combine(assetGroup.Settings.buildSettings.bundleBuildPath, buildBundles[i]); if (assetGroup.GetSchema()?.BundleNaming == BundledAssetGroupSchema.BundleNamingStyle.NoHash) outputBundles[i] = StripHashFromBundleLocation(outputBundles[i]); bundleRenameMap.Add(buildBundles[i], outputBundles[i]); MoveFileToDestinationWithTimestampIfDifferent(srcPath, targetPath, m_Log); AddPostCatalogUpdatesInternal(assetGroup, postCatalogUpdateCallbacks, dataEntry, targetPath, registry); registry.AddFile(targetPath); } } internal void AddPostCatalogUpdatesInternal(AddressableAssetGroup assetGroup, List postCatalogUpdates, ContentCatalogDataEntry dataEntry, string targetBundlePath, FileRegistry registry) { if (assetGroup.GetSchema()?.BundleNaming == BundledAssetGroupSchema.BundleNamingStyle.NoHash) { postCatalogUpdates.Add(() => { //This is where we strip out the temporary hash for the final bundle location and filename string bundlePathWithoutHash = StripHashFromBundleLocation(targetBundlePath); if (File.Exists(targetBundlePath)) { if (File.Exists(bundlePathWithoutHash)) File.Delete(bundlePathWithoutHash); string destFolder = Path.GetDirectoryName(bundlePathWithoutHash); if (!string.IsNullOrEmpty(destFolder) && !Directory.Exists(destFolder)) Directory.CreateDirectory(destFolder); File.Move(targetBundlePath, bundlePathWithoutHash); } if (registry != null) { if (!registry.ReplaceBundleEntry(targetBundlePath, bundlePathWithoutHash)) Debug.LogErrorFormat("Unable to find registered file for bundle {0}.", targetBundlePath); } if (dataEntry != null) if (DataEntryDiffersFromBundleFilename(dataEntry, bundlePathWithoutHash)) dataEntry.InternalId = StripHashFromBundleLocation(dataEntry.InternalId); }); } } // if false, there is no need to remove the hash from dataEntry.InternalId bool DataEntryDiffersFromBundleFilename(ContentCatalogDataEntry dataEntry, string bundlePathWithoutHash) { string dataEntryId = dataEntry.InternalId; string dataEntryFilename = Path.GetFileName(dataEntryId); string bundleFileName = Path.GetFileName(bundlePathWithoutHash); return dataEntryFilename != bundleFileName; } /// /// Creates a name for an asset bundle using the provided information. /// /// The asset group. /// The schema of the group. /// The bundle information. /// The base name of the asset bundle. /// Returns the asset bundle name with the provided information. protected virtual string ConstructAssetBundleName(AddressableAssetGroup assetGroup, BundledAssetGroupSchema schema, BundleDetails info, string assetBundleName) { if (assetGroup != null) { string groupName = assetGroup.Name.Replace(" ", "").Replace('\\', '/').Replace("//", "/").ToLower(); assetBundleName = groupName + "_" + assetBundleName; } string bundleNameWithHashing = BuildUtility.GetNameWithHashNaming(schema.BundleNaming, info.Hash.ToString(), assetBundleName); //For no hash, we need the hash temporarily for content update purposes. This will be stripped later on. if (schema.BundleNaming == BundledAssetGroupSchema.BundleNamingStyle.NoHash) { bundleNameWithHashing = bundleNameWithHashing.Replace(".bundle", "_" + info.Hash.ToString() + ".bundle"); } return bundleNameWithHashing; } static void ReplaceDependencyKeys(string from, string to, List locations) { foreach (ContentCatalogDataEntry location in locations) { for (int i = 0; i < location.Dependencies.Count; ++i) { string s = location.Dependencies[i] as string; if (string.IsNullOrEmpty(s)) continue; if (s == from) location.Dependencies[i] = to; } } } private static long GetFileSize(string fileName) { try { return new FileInfo(fileName).Length; } catch (Exception e) { Debug.LogException(e); return 0; } } /// public override void ClearCachedData() { if (Directory.Exists(Addressables.BuildPath)) { try { var catalogPath = Addressables.BuildPath + "/catalog.json"; var settingsPath = Addressables.BuildPath + "/settings.json"; DeleteFile(catalogPath); DeleteFile(settingsPath); Directory.Delete(Addressables.BuildPath, true); } catch (Exception e) { Debug.LogException(e); } } } /// public override bool IsDataBuilt() { var settingsPath = Addressables.BuildPath + "/settings.json"; return !String.IsNullOrEmpty(m_CatalogBuildPath) && File.Exists(m_CatalogBuildPath) && File.Exists(settingsPath); } } }