using System; using System.Collections.Generic; using System.IO; using System.Linq; #if UNITY_2022_2_OR_NEWER using UnityEditor.AddressableAssets.BuildReportVisualizer; #endif using UnityEditor.AddressableAssets.Settings; using UnityEditor.AddressableAssets.Settings.GroupSchemas; using UnityEditor.Build.Pipeline.Interfaces; using UnityEditor.Build.Pipeline.Utilities; using UnityEditor.Experimental; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.AddressableAssets.Initialization; using UnityEngine.AddressableAssets.ResourceLocators; using UnityEngine.ResourceManagement.ResourceProviders; using UnityEngine.ResourceManagement.Util; using UnityEngine.Serialization; using System.Reflection; namespace UnityEditor.AddressableAssets.Build.DataBuilders { /// /// Base class for build script assets /// public class BuildScriptBase : ScriptableObject, IDataBuilder { /// /// Static part of the builtin bundle filename. /// public const string BuiltInBundleBaseName = "_unitybuiltinassets"; /// /// The type of instance provider to create for the Addressables system. /// [FormerlySerializedAs("m_InstanceProviderType")] [SerializedTypeRestrictionAttribute(type = typeof(IInstanceProvider))] public SerializedType instanceProviderType = new SerializedType() { Value = typeof(InstanceProvider) }; /// /// The type of scene provider to create for the addressables system. /// [FormerlySerializedAs("m_SceneProviderType")] [SerializedTypeRestrictionAttribute(type = typeof(ISceneProvider))] public SerializedType sceneProviderType = new SerializedType() { Value = typeof(SceneProvider) }; /// /// Stores the logged information of all the build tasks. /// public IBuildLogger Log { get { return m_Log; } } [NonSerialized] internal IBuildLogger m_Log; /// /// The descriptive name used in the UI. /// public virtual string Name { get { return "Undefined"; } } internal static void WriteBuildLog(BuildLog log, string directory) { Directory.CreateDirectory(directory); PackageManager.PackageInfo info = PackageManager.PackageInfo.FindForAssembly(typeof(BuildScriptBase).Assembly); log.AddMetaData(info.name, info.version); File.WriteAllText(Path.Combine(directory, "AddressablesBuildTEP.json"), log.FormatForTraceEventProfiler()); } /// /// Build the specified data with the provided builderInput. This is the public entry point. /// Child class overrides should use /// /// The type of data to build. /// The builderInput object used in the build. /// The build data result. public TResult BuildData(AddressablesDataBuilderInput builderInput) where TResult : IDataBuilderResult { if (!CanBuildData()) { var message = "Data builder " + Name + " cannot build requested type: " + typeof(TResult); Debug.LogError(message); return AddressableAssetBuildResult.CreateResult(null, 0, message); } AddressableAnalytics.BuildType buildType = AddressableAnalytics.DetermineBuildType(); m_Log = (builderInput.Logger != null) ? builderInput.Logger : new BuildLog(); AddressablesRuntimeProperties.ClearCachedPropertyValues(); TResult result = default; // Append the file registry to the results using (m_Log.ScopedStep(LogLevel.Info, $"Building {this.Name}")) { try { result = BuildDataImplementation(builderInput); } catch (Exception e) { string errMessage; if (e.Message == "path") errMessage = "Invalid path detected during build. Check for unmatched brackets in your active profile's variables."; else errMessage = e.Message; Debug.LogError(errMessage); return AddressableAssetBuildResult.CreateResult(null, 0, errMessage); } if (result != null) result.FileRegistry = builderInput.Registry; } if (builderInput.Logger == null && m_Log != null) WriteBuildLog((BuildLog)m_Log, Path.GetDirectoryName(Application.dataPath) + "/" + Addressables.LibraryPath); if (result is AddressableAssetBuildResult) { AddressableAnalytics.ReportBuildEvent(builderInput, result as AddressableAssetBuildResult, buildType); } return result; } /// /// The implementation of . That is the public entry point, /// this is the home for child class overrides. /// /// The builderInput object used in the build /// The type of data to build /// The build data result protected virtual TResult BuildDataImplementation(AddressablesDataBuilderInput builderInput) where TResult : IDataBuilderResult { return default(TResult); } /// /// Loops over each group, after doing some data checking. /// /// The Addressables builderInput object to base the group processing on /// An error string if there were any problems processing the groups protected virtual string ProcessAllGroups(AddressableAssetsBuildContext aaContext) { try { if (aaContext == null || aaContext.Settings == null || aaContext.Settings.groups == null) { return "No groups found to process in build script " + Name; } //intentionally for not foreach so groups can be added mid-loop. for (int index = 0; index < aaContext.Settings.groups.Count; index++) { AddressableAssetGroup assetGroup = aaContext.Settings.groups[index]; if (assetGroup == null) continue; var error = ErrorCheckBundleSettings(assetGroup, aaContext); if (error != string.Empty) { return error; } EditorUtility.DisplayProgressBar($"Processing Addressable Group", assetGroup.Name, (float)index / aaContext.Settings.groups.Count); var errorString = ProcessGroup(assetGroup, aaContext); if (!string.IsNullOrEmpty(errorString)) { return errorString; } } } finally { EditorUtility.ClearProgressBar(); } return string.Empty; } // If user has recently customized the build/load path in the inspector, it may not be retrievable from profileSettings yet // We retrieve it directly from the build/load path variable in that case. internal static string GetCustomOrDefaultPath(AddressableAssetSettings settings, ProfileValueReference pathValueReference, bool useCustomPaths) { if (useCustomPaths) { return pathValueReference.GetValue(settings, false); } return settings.profileSettings.GetValueById(settings.activeProfileId, pathValueReference.Id); } internal static string ErrorCheckBundleSettings(AddressableAssetGroup assetGroup, AddressableAssetsBuildContext aaContext) { if (!assetGroup.HasSchema()) return string.Empty; var message = string.Empty; var settings = aaContext.Settings; var schema = assetGroup.GetSchema(); if (settings.UseUnityWebRequestForLocalBundles && schema.StripDownloadOptions) { message = "Strip Download Options is enabled, but Use UnityWebRequest for Local Bundles is also enabled. " + "These options are mutually exclusive and cannot be used together."; } string buildPath = GetCustomOrDefaultPath(settings, schema.BuildPath, schema.m_UseCustomPaths); string loadPath = GetCustomOrDefaultPath(settings, schema.LoadPath, schema.m_UseCustomPaths); bool buildLocal = AddressableAssetUtility.StringContains(buildPath, "[UnityEngine.AddressableAssets.Addressables.BuildPath]", StringComparison.Ordinal); bool loadLocal = AddressableAssetUtility.StringContains(loadPath, "{UnityEngine.AddressableAssets.Addressables.RuntimePath}", StringComparison.Ordinal); 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; } /// /// Build processing of an individual group. /// /// The group to process /// The Addressables builderInput object to base the group processing on /// An error string if there were any problems processing the groups protected virtual string ProcessGroup(AddressableAssetGroup assetGroup, AddressableAssetsBuildContext aaContext) { return string.Empty; } /// /// Used to determine if this builder is capable of building a specific type of data. /// /// The type of data needed to be built. /// True if this builder can build this data. public virtual bool CanBuildData() where T : IDataBuilderResult { return false; } /// /// Utility method for deleting files. /// /// The file path to delete. protected static void DeleteFile(string path) { try { if (File.Exists(path)) File.Delete(path); } catch (Exception ex) { Debug.LogException(ex); } } /// /// Utility method to write a file. The directory will be created if it does not exist. /// /// The path of the file to write. /// The content of the file. /// The file registry used to track all produced artifacts. /// True if the file was written. protected internal static bool WriteFile(string path, byte[] content, FileRegistry registry) { try { registry.AddFile(path); var dir = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir); File.WriteAllBytes(path, content); return true; } catch (Exception ex) { Debug.LogException(ex); registry.RemoveFile(path); return false; } } /// /// Utility method to write a file. The directory will be created if it does not exist. /// /// The path of the file to write. /// The content of the file. /// The file registry used to track all produced artifacts. /// True if the file was written. protected static bool WriteFile(string path, string content, FileRegistry registry) { try { registry.AddFile(path); var dir = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir); File.WriteAllText(path, content); return true; } catch (Exception ex) { Debug.LogException(ex); registry.RemoveFile(path); return false; } } /// /// Used to clean up any cached data created by this builder. /// public virtual void ClearCachedData() { } /// /// Checks to see if the data is built for the given builder. /// /// Returns true if the data is built. Returns false otherwise. public virtual bool IsDataBuilt() { return false; } /// /// Copies the content state binary file from the temp directory to its final location and registers it in the /// file registry and build results. /// /// Temporary location of the content state file. /// Destination location of the content state file. /// The builderInput object used in the build. /// The build data result. public virtual void CopyAndRegisterContentState(string tempPath, string contentStatePath, AddressablesDataBuilderInput builderInput, AddressablesPlayerBuildResult addrResult) { try { string directory = Path.GetDirectoryName(contentStatePath); if (!Directory.Exists(directory)) Directory.CreateDirectory(directory); if (File.Exists(contentStatePath)) File.Delete(contentStatePath); File.Copy(tempPath, contentStatePath, true); if (addrResult != null) addrResult.ContentStateFilePath = contentStatePath; 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); } } /// /// Notifies the user about the existence of the Addressables Report /// protected virtual void NotifyUserAboutBuildReport() { bool buildReportSettingCheck = ProjectConfigData.UserHasBeenInformedAboutBuildReportSettingPreBuild; if (!buildReportSettingCheck && !Application.isBatchMode && !ProjectConfigData.GenerateBuildLayout) { bool turnOnBuildLayout = EditorUtility.DisplayDialog("Addressables Build Report", "There's a new Addressables Build Report you can check out after your content build. " + "However, this requires that 'Debug Build Layout' is turned on. The setting can be found in Edit > Preferences > Addressables. Would you like to turn it on?", "Yes", "No"); if (turnOnBuildLayout) ProjectConfigData.GenerateBuildLayout = true; ProjectConfigData.UserHasBeenInformedAboutBuildReportSettingPreBuild = true; } } /// /// Displays the Addressables Report window /// protected virtual void DisplayBuildReport() { if (!Application.isBatchMode && ProjectConfigData.AutoOpenAddressablesReport && ProjectConfigData.GenerateBuildLayout) { BuildReportWindow.ShowWindowAfterBuild(); } } /// /// Clears content update notifications from teh groups window /// /// A list of groups that were built protected virtual void ClearContentUpdateNotifications(List groups) { foreach (var group in groups) ContentUpdateScript.ClearContentUpdateNotifications(group); } } }