diff --git a/Editor/Mono/Audio/AudioContainerWindow.cs b/Editor/Mono/Audio/AudioContainerWindow.cs index be0861dcae..55e043b889 100644 --- a/Editor/Mono/Audio/AudioContainerWindow.cs +++ b/Editor/Mono/Audio/AudioContainerWindow.cs @@ -965,6 +965,11 @@ void OnAudioClipListChanged(SerializedProperty property) // Force a list rebuild when the list has changed or it will not always render correctly m_ClipsListView.Rebuild(); + // This function is the first entry-point in `AudioContainerWindow` after an undo-event that alters the + // audio clip list has been triggered. And, whenever the list is altered, we need to make sure the state is stopped. + State.Stop(); + ClearClipFieldProgressBars(); + UpdateTransportButtonStates(); SetTitle(); } diff --git a/Editor/Mono/BuildPipeline.bindings.cs b/Editor/Mono/BuildPipeline.bindings.cs index f159eb1b07..a114434beb 100644 --- a/Editor/Mono/BuildPipeline.bindings.cs +++ b/Editor/Mono/BuildPipeline.bindings.cs @@ -393,6 +393,24 @@ private static BuildReport BuildPlayer(string[] scenes, string locationPathName, scenes[i] = scenes[i].Replace('\\', '/').Replace("//", "/"); } + if ((options & BuildOptions.Development) == 0) + { + if ((options & BuildOptions.AllowDebugging) != 0) + { + throw new ArgumentException("Non-development build cannot allow debugging. Either add the Development build option, or remove the AllowDebugging build option."); + } + + if ((options & BuildOptions.EnableDeepProfilingSupport) != 0) + { + throw new ArgumentException("Non-development build cannot allow deep profiling support. Either add the Development build option, or remove the EnableDeepProfilingSupport build option."); + } + + if ((options & BuildOptions.ConnectWithProfiler) != 0) + { + throw new ArgumentException("Non-development build cannot allow auto-connecting the profiler. Either add the Development build option, or remove the ConnectWithProfiler build option."); + } + } + try { return BuildPlayerInternal(scenes, locationPathName, assetBundleManifestPath, buildTargetGroup, target, subtarget, options, extraScriptingDefines); diff --git a/Editor/Mono/BuildProfile/BuildProfile.cs b/Editor/Mono/BuildProfile/BuildProfile.cs index baac9a08a6..9098b2e5ab 100644 --- a/Editor/Mono/BuildProfile/BuildProfile.cs +++ b/Editor/Mono/BuildProfile/BuildProfile.cs @@ -153,14 +153,23 @@ internal PlayerSettings playerSettings [VisibleToOtherModules] internal Action OnPlayerSettingsUpdatedFromYAML; + /// + /// Cross-pipeline graphics settings overrides in build profile + /// + [VisibleToOtherModules] + internal BuildProfileGraphicsSettings graphicsSettings; + [VisibleToOtherModules] internal Action OnGraphicsSettingsSubAssetRemoved; /// - /// Cross-pipeline graphics settings overrides in build profile + /// Quality settings overrides in build profile /// [VisibleToOtherModules] - internal BuildProfileGraphicsSettings graphicsSettings; + internal BuildProfileQualitySettings qualitySettings; + + [VisibleToOtherModules] + internal Action OnQualitySettingsSubAssetRemoved; // TODO: Return server IBuildTargets for server build profiles. (https://jira.unity3d.com/browse/PLAT-6612) /// @@ -235,6 +244,7 @@ void OnEnable() LoadPlayerSettings(); TryLoadGraphicsSettings(); + TryLoadQualitySettings(); if (!EditorUserBuildSettings.isBuildProfileAvailable || BuildProfileContext.activeProfile != this) @@ -266,6 +276,21 @@ void TryLoadGraphicsSettings() graphicsSettings = data; } + void TryLoadQualitySettings() + { + if (qualitySettings != null) + return; + + var path = AssetDatabase.GetAssetPath(this); + var objects = AssetDatabase.LoadAllAssetsAtPath(path); + + var data = Array.Find(objects, obj => obj is BuildProfileQualitySettings) as BuildProfileQualitySettings; + if (data == null) + return; + + qualitySettings = data; + } + void OnDisable() { if (BuildProfileContext.activeProfile == this) @@ -297,6 +322,7 @@ static void ContextMenuReset(MenuCommand menuCommand) targetBuildProfile.scriptingDefines = Array.Empty(); BuildProfileModuleUtil.RemovePlayerSettings(targetBuildProfile); + targetBuildProfile.RemoveQualitySettings(); targetBuildProfile.RemoveGraphicsSettings(); AssetDatabase.SaveAssetIfDirty(targetBuildProfile); diff --git a/Editor/Mono/BuildProfile/BuildProfileContext.cs b/Editor/Mono/BuildProfile/BuildProfileContext.cs index 1c78527f6f..567fc10bba 100644 --- a/Editor/Mono/BuildProfile/BuildProfileContext.cs +++ b/Editor/Mono/BuildProfile/BuildProfileContext.cs @@ -314,6 +314,17 @@ internal static bool ActiveProfileHasGraphicsSettings() return activeProfile.graphicsSettings != null; } + /// + /// Check if the active build profile has quality settings + /// + internal static bool ActiveProfileHasQualitySettings() + { + if (activeProfile == null) + return false; + + return activeProfile.qualitySettings != null; + } + /// /// Sync the active build profile to EditorUserBuildSettings to ensure they are in a consistent state. /// @@ -734,6 +745,24 @@ static bool SetActiveShaderVariantCollections(ShaderVariantCollection[] collecti return true; } + [RequiredByNativeCode, UsedImplicitly] + static string[] GetActiveProfileQualityLevels() + { + if (!ActiveProfileHasQualitySettings()) + return Array.Empty(); + + return activeProfile.qualitySettings.qualityLevels; + } + + [RequiredByNativeCode, UsedImplicitly] + static string GetActiveProfileDefaultQualityLevel() + { + if (!ActiveProfileHasQualitySettings()) + return string.Empty; + + return activeProfile.qualitySettings.defaultQualityLevel; + } + [RequiredByNativeCode] static string GetActiveBuildProfilePath() { diff --git a/Editor/Mono/BuildProfile/BuildProfileCreate.cs b/Editor/Mono/BuildProfile/BuildProfileCreate.cs index abdb4e306a..a3c0acd8e5 100644 --- a/Editor/Mono/BuildProfile/BuildProfileCreate.cs +++ b/Editor/Mono/BuildProfile/BuildProfileCreate.cs @@ -102,5 +102,20 @@ internal void RemoveGraphicsSettings() OnGraphicsSettingsSubAssetRemoved?.Invoke(); } + + /// + /// Remove the Quality Settings overrides from the build profile. + /// + internal void RemoveQualitySettings() + { + if (qualitySettings == null) + return; + + AssetDatabase.RemoveObjectFromAsset(qualitySettings); + qualitySettings = null; + EditorUtility.SetDirty(this); + + OnQualitySettingsSubAssetRemoved?.Invoke(); + } } } diff --git a/Editor/Mono/BuildProfile/BuildProfileModuleUtil.cs b/Editor/Mono/BuildProfile/BuildProfileModuleUtil.cs index ae9f2df67c..d61c6de0e0 100644 --- a/Editor/Mono/BuildProfile/BuildProfileModuleUtil.cs +++ b/Editor/Mono/BuildProfile/BuildProfileModuleUtil.cs @@ -608,9 +608,81 @@ public static void OnActiveProfileGraphicsSettingsChanged(bool hasGraphicsSettin GraphicsSettingsInspector.OnActiveProfileGraphicsSettingsChanged?.Invoke(); } + internal static void RemoveQualityLevelFromAllProfiles(string qualityLevelName) + { + var profiles = GetAllBuildProfiles(); + foreach (var profile in profiles) + { + if (profile.qualitySettings == null) + continue; + + profile.qualitySettings.RemoveQualityLevel(qualityLevelName); + } + } + + internal static void RenameQualityLevelInAllProfiles(string oldName, string newName) + { + var profiles = GetAllBuildProfiles(); + foreach (var profile in profiles) + { + if (profile.qualitySettings == null) + continue; + + profile.qualitySettings.RenameQualityLevel(oldName, newName); + } + } + + /// + /// Get all custom build profiles in the project. + /// + public static List GetAllBuildProfiles() + { + var alreadyLoadedBuildProfiles = Resources.FindObjectsOfTypeAll(); + + const string buildProfileAssetSearchString = $"t:{nameof(BuildProfile)}"; + var assetsGuids = AssetDatabase.FindAssets(buildProfileAssetSearchString); + var result = new List(assetsGuids.Length); + + // Suppress missing type warning thrown by serialization. This could happen + // when the build profile window is opened, then entering play mode and the + // module for that profile is not installed. + BuildProfileModuleUtil.SuppressMissingTypeWarning(); + + foreach (var guid in assetsGuids) + { + string path = AssetDatabase.GUIDToAssetPath(guid); + BuildProfile profile = AssetDatabase.LoadAssetAtPath(path); + if (profile == null) + { + Debug.LogWarning($"[BuildProfile] Failed to load asset at path: {path}"); + continue; + } + + result.Add(profile); + } + + foreach (var buildProfile in alreadyLoadedBuildProfiles) + { + // Asset database will not give us any build profiles that get created in memory + // and we need to include them in this list as we use it to detect that build profiles + // have been destroyed and destroy their resources like PlayerSettings afterwards. + // Skipping the in-memory build profiles will result in us deleting their associated + // player settings object while it's being used and will lead to a crash (UUM-77423) + if (buildProfile && + !BuildProfileContext.IsClassicPlatformProfile(buildProfile) && + !BuildProfileContext.IsSharedProfile(buildProfile.buildTarget) && + !EditorUtility.IsPersistent(buildProfile)) + { + result.Add(buildProfile); + } + } + + return result; + } + public static string[] GetSettingsRequiringRestart(PlayerSettings previousProfileSettings, PlayerSettings newProfileSettings, BuildTarget oldBuildTarget, BuildTarget newBuildTarget) { - return PlayerSettings.GetSettingsRequiringRestart(previousProfileSettings, newProfileSettings, oldBuildTarget, newBuildTarget); + return PlayerSettings.GetSettingsRequiringRestart(previousProfileSettings, newProfileSettings, oldBuildTarget, newBuildTarget); } public static PlayerSettings GetGlobalPlayerSettings() diff --git a/Editor/Mono/BuildProfile/BuildProfileQualitySettings.cs b/Editor/Mono/BuildProfile/BuildProfileQualitySettings.cs new file mode 100644 index 0000000000..7d841df0ad --- /dev/null +++ b/Editor/Mono/BuildProfile/BuildProfileQualitySettings.cs @@ -0,0 +1,66 @@ +// Unity C# reference source +// Copyright (c) Unity Technologies. For terms of use, see +// https://unity3d.com/legal/licenses/Unity_Reference_Only_License + +using System; +using UnityEngine; +using UnityEngine.Bindings; + +namespace UnityEditor.Build.Profile +{ + [VisibleToOtherModules("UnityEditor.BuildProfileModule")] + sealed class BuildProfileQualitySettings : ScriptableObject + { + [SerializeField] string m_DefaultQualityLevel = string.Empty; + [SerializeField] string[] m_QualityLevels = Array.Empty(); + + public string defaultQualityLevel + { + get => m_DefaultQualityLevel; + set => m_DefaultQualityLevel = value; + } + + public string[] qualityLevels + { + get => m_QualityLevels; + set => m_QualityLevels = value; + } + + public void Instantiate() + { + name = "Quality Settings"; + hideFlags = HideFlags.HideInHierarchy | HideFlags.HideInInspector; + } + + public void RemoveQualityLevel(string qualityLevel) + { + var index = Array.IndexOf(qualityLevels, qualityLevel); + if (index == -1) + return; + + var newQualityLevels = new string[qualityLevels.Length - 1]; + Array.Copy(qualityLevels, 0, newQualityLevels, 0, index); + Array.Copy(qualityLevels, index + 1, newQualityLevels, index, qualityLevels.Length - index - 1); + qualityLevels = newQualityLevels; + + if (defaultQualityLevel == qualityLevel) + defaultQualityLevel = qualityLevels.Length > 0 ? qualityLevels[0] : string.Empty; + + EditorUtility.SetDirty(this); + } + + public void RenameQualityLevel(string oldName, string newName) + { + var index = Array.IndexOf(qualityLevels, oldName); + if (index == -1) + return; + + qualityLevels[index] = newName; + + if (defaultQualityLevel == oldName) + defaultQualityLevel = newName; + + EditorUtility.SetDirty(this); + } + } +} diff --git a/Editor/Mono/BuildProfile/BuildProfileQualitySettingsEditor.cs b/Editor/Mono/BuildProfile/BuildProfileQualitySettingsEditor.cs new file mode 100644 index 0000000000..a6fa7ad028 --- /dev/null +++ b/Editor/Mono/BuildProfile/BuildProfileQualitySettingsEditor.cs @@ -0,0 +1,293 @@ +// Unity C# reference source +// Copyright (c) Unity Technologies. For terms of use, see +// https://unity3d.com/legal/licenses/Unity_Reference_Only_License + +using System; +using UnityEditor.UIElements; +using UnityEngine; +using UnityEngine.Bindings; +using UnityEngine.UIElements; + +namespace UnityEditor.Build.Profile +{ + [VisibleToOtherModules("UnityEditor.BuildProfileModule")] + [CustomEditor(typeof(BuildProfileQualitySettings))] + sealed class BuildProfileQualitySettingsEditor : Editor + { + const string k_Uxml = "BuildProfile/UXML/BuildProfileQualitySettings.uxml"; + const string k_StyleSheet = "BuildProfile/StyleSheets/BuildProfile.uss"; + const string k_QualitySettingsWindow = "Project/Quality"; + static readonly GUIContent k_qualitySettingsWindow = EditorGUIUtility.TrTextContent("Quality..."); + static readonly string k_InvalidQualityLevelWarning = + L10n.Tr("The Quality levels in this profile do not match those that exist in the project. This may result in unexpected results on build."); + static readonly string k_EmptyQualitySettingsWarning = + L10n.Tr("When no Quality levels are listed, the build will take from the global list of Quality levels."); + static readonly string k_SetDefaultQualityLevelMenuText = L10n.Tr("Set as Default"); + + SerializedProperty m_QualityLevels; + SerializedProperty m_DefaultQualityLevel; + HelpBox warning; + + public override VisualElement CreateInspectorGUI() + { + var root = new VisualElement(); + var visualTree = EditorGUIUtility.LoadRequired(k_Uxml) as VisualTreeAsset; + var windowUss = EditorGUIUtility.LoadRequired(k_StyleSheet) as StyleSheet; + visualTree.CloneTree(root); + root.styleSheets.Add(windowUss); + + m_QualityLevels = serializedObject.FindProperty("m_QualityLevels"); + m_DefaultQualityLevel = serializedObject.FindProperty("m_DefaultQualityLevel"); + + root.Bind(serializedObject); + SetupQualityLevelsList(root); + + return root; + } + + void SetupQualityLevelsList(VisualElement root) + { + warning = root.Q("invalid-quality-levels-warning-help-box"); + UpdateInvalidQualityLevelsWarning(); + + var qualityLevelsList = root.Q("quality-levels"); + root.TrackPropertyValue(m_QualityLevels, sp => { + UpdateInvalidQualityLevelsWarning(); + qualityLevelsList.RefreshItems(); + }); + root.TrackPropertyValue(m_DefaultQualityLevel, sp => qualityLevelsList.RefreshItems()); + + qualityLevelsList.makeItem = () => new QualityLevelItem(SetDefaultQualityLevelContextMenu()); + qualityLevelsList.bindItem = (element, index) => + { + if (m_QualityLevels.arraySize == 0 || index >= m_QualityLevels.arraySize) + return; + + var item = element as QualityLevelItem; + var qualityLevelName = m_QualityLevels.GetArrayElementAtIndex(index).stringValue; + + item.text = qualityLevelName; + if (IsDefaultQualityLevel(qualityLevelName)) + item.SetDefaultIndicator(true); + else + item.SetDefaultIndicator(false); + }; + qualityLevelsList.onAdd = list => + { + var menu = new GenericMenu(); + var allQualityLevels = QualitySettings.names; + foreach (var level in allQualityLevels) + { + if (!IsQualityLevelAdded(level)) + menu.AddItem(new GUIContent(level), false, () => AddQualityLevel(level)); + } + + menu.AddSeparator(string.Empty); + menu.AddItem(k_qualitySettingsWindow, false, () => SettingsService.OpenProjectSettings(k_QualitySettingsWindow)); + menu.ShowAsContext(); + }; + qualityLevelsList.onRemove = list => + { + RemoveQualityLevel(); + }; + + void AddQualityLevel(string newLevel) + { + m_QualityLevels.InsertArrayElementAtIndex(m_QualityLevels.arraySize); + m_QualityLevels.GetArrayElementAtIndex(m_QualityLevels.arraySize - 1).stringValue = newLevel; + + if (m_QualityLevels.arraySize == 1) + m_DefaultQualityLevel.stringValue = newLevel; + + serializedObject.ApplyModifiedProperties(); + } + + void RemoveQualityLevel() + { + if (m_QualityLevels.arraySize == 0) + return; + + var selectedIndex = qualityLevelsList.selectedIndex; + + if (selectedIndex < 0 || selectedIndex >= m_QualityLevels.arraySize) + selectedIndex = m_QualityLevels.arraySize - 1; + + if (selectedIndex >= 0) + { + var deletedQualityLevel = m_QualityLevels.GetArrayElementAtIndex(selectedIndex).stringValue; + m_QualityLevels.DeleteArrayElementAtIndex(selectedIndex); + + if (IsDefaultQualityLevel(deletedQualityLevel)) + m_DefaultQualityLevel.stringValue = m_QualityLevels.arraySize > 0 ? + m_QualityLevels.GetArrayElementAtIndex(0).stringValue : string.Empty; + + serializedObject.ApplyModifiedProperties(); + } + } + } + + bool IsDefaultQualityLevel(string qualityLevel) => m_DefaultQualityLevel.stringValue == qualityLevel; + + bool IsQualityLevelAdded(string level) + { + for (var i = 0; i < m_QualityLevels.arraySize; i++) + { + if (m_QualityLevels.GetArrayElementAtIndex(i).stringValue == level) + return true; + } + + return false; + } + + void UpdateInvalidQualityLevelsWarning() + { + if (m_QualityLevels.arraySize == 0) + { + warning.text = k_EmptyQualitySettingsWarning; + warning.style.display = DisplayStyle.Flex; + } + else if (HasInvalidQualityLevels()) + { + warning.text = k_InvalidQualityLevelWarning; + warning.style.display = DisplayStyle.Flex; + } + else + warning.style.display = DisplayStyle.None; + } + + bool HasInvalidQualityLevels() + { + var allQualityLevels = QualitySettings.names; + for (var i = 0; i < m_QualityLevels.arraySize; i++) + { + var level = m_QualityLevels.GetArrayElementAtIndex(i).stringValue; + if (Array.IndexOf(allQualityLevels, level) == -1) + return true; + } + + return false; + } + + ContextualMenuManipulator SetDefaultQualityLevelContextMenu() + { + var menu = new ContextualMenuManipulator(evt => + { + var selectedQualityLevel = evt.target as QualityLevelItem; + if (selectedQualityLevel == null) + return; + + evt.menu.AppendAction(k_SetDefaultQualityLevelMenuText, action => + { + m_DefaultQualityLevel.stringValue = selectedQualityLevel.text; + serializedObject.ApplyModifiedProperties(); + }); + }); + + return menu; + } + + public bool IsDataEqualToGlobalQualitySettings(BuildProfile profile) + { + var buildTarget = profile.buildTarget; + var buildTargetGroupString = BuildPipeline.GetBuildTargetGroup(buildTarget).ToString(); + + var globalQualityLevels = QualitySettings.GetActiveQualityLevelsForPlatform(buildTargetGroupString); + if (m_QualityLevels.arraySize != globalQualityLevels.Length) + return false; + + var allQualityLevels = QualitySettings.names; + for (var i = 0; i < m_QualityLevels.arraySize; i++) + { + var levelIndex = globalQualityLevels[i]; + if (m_QualityLevels.GetArrayElementAtIndex(i).stringValue != allQualityLevels[levelIndex]) + return false; + } + + var globalDefaultQualityLevelIndex = GetDefaultQualityForPlatform(buildTargetGroupString); + if (globalDefaultQualityLevelIndex != -1) + { + var globalDefaultQualityLevel = allQualityLevels[globalDefaultQualityLevelIndex]; + if (m_DefaultQualityLevel.stringValue != globalDefaultQualityLevel) + return false; + } + + return true; + } + + public void ResetToGlobalQualitySettingsValues(BuildProfile profile) + { + var qualityLevels = serializedObject.FindProperty("m_QualityLevels"); + var defaultQualityLevel = serializedObject.FindProperty("m_DefaultQualityLevel"); + var buildTarget = profile.buildTarget; + var buildTargetGroupString = BuildPipeline.GetBuildTargetGroup(buildTarget).ToString(); + + var globalQualityLevels = QualitySettings.GetActiveQualityLevelsForPlatform(buildTargetGroupString); + qualityLevels.ClearArray(); + foreach (var level in globalQualityLevels) + { + qualityLevels.InsertArrayElementAtIndex(qualityLevels.arraySize); + qualityLevels.GetArrayElementAtIndex(qualityLevels.arraySize - 1).stringValue = QualitySettings.names[level]; + } + + var globalDefaultQualityLevelIndex = GetDefaultQualityForPlatform(buildTargetGroupString); + if (globalDefaultQualityLevelIndex != -1) + defaultQualityLevel.stringValue = QualitySettings.names[globalDefaultQualityLevelIndex]; + else + defaultQualityLevel.stringValue = qualityLevels.arraySize > 0 ? QualitySettings.names[globalQualityLevels[0]] : string.Empty; + + serializedObject.ApplyModifiedProperties(); + } + + int GetDefaultQualityForPlatform(string buildTargetGroupName) + { + var qualitySettings = QualitySettings.GetQualitySettings(); + var qualitySettingsSO = new SerializedObject(qualitySettings); + var perPlatformDefaultQualityProperty = qualitySettingsSO.FindProperty("m_PerPlatformDefaultQuality"); + + foreach (SerializedProperty prop in perPlatformDefaultQualityProperty) + { + if (prop.FindPropertyRelative("first").stringValue == buildTargetGroupName) + return prop.FindPropertyRelative("second").intValue; + } + return -1; + } + + class QualityLevelItem : VisualElement + { + const string k_Uxml = "BuildProfile/UXML/BuildProfileQualitySettingsListElement.uxml"; + const string k_StyleSheet = "BuildProfile/StyleSheets/BuildProfile.uss"; + static readonly string k_DefaultIndicatorText = L10n.Tr("Default"); + protected readonly Label m_Text; + protected readonly Label m_DefaultIndicator; + + internal string text + { + get => m_Text.text; + set => m_Text.text = value; + } + + internal QualityLevelItem(IManipulator manipulator) + { + var uxml = EditorGUIUtility.LoadRequired(k_Uxml) as VisualTreeAsset; + var stylesheet = EditorGUIUtility.LoadRequired(k_StyleSheet) as StyleSheet; + styleSheets.Add(stylesheet); + uxml.CloneTree(this); + + m_Text = this.Q