using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEditor.Build; using UnityEditor.XR.OpenXR; using UnityEngine; using UnityEngine.XR.OpenXR; using UnityEngine.XR.OpenXR.Features; namespace UnityEditor.XR.OpenXR.Features { /// /// API for finding and managing feature sets for OpenXR. /// public static class OpenXRFeatureSetManager { [InitializeOnLoadMethod] static void InitializeOnLoad() { void OnFirstUpdate() { EditorApplication.update -= OnFirstUpdate; InitializeFeatureSets(); } OpenXRFeature.canSetFeatureDisabled = CanFeatureBeDisabled; EditorApplication.update += OnFirstUpdate; } /// /// Description of a known (either built-in or found) feature set. /// public class FeatureSet { /// /// Toggles the enabled state for this feature. Impacts the effect of . /// If you change this value, you must call to reflect that change on the actual feature sets. /// public bool isEnabled; /// /// The name that displays in the UI. /// public string name; /// /// Description of this feature set. /// public string description; /// /// The feature set id as defined in . /// public string featureSetId; /// /// The text to be shown with the . /// public string downloadText; /// /// The URI string used to link to external documentation. /// public string downloadLink; /// /// The set of features that this feature set menages. /// public string[] featureIds; /// /// State that tracks whether this feature set is built in or was detected after the user installed it. /// public bool isInstalled; /// /// The set of required features that this feature set manages. /// public string[] requiredFeatureIds; /// /// The set of default features that this feature set manages. /// public string[] defaultFeatureIds; } internal class FeatureSetInfo : FeatureSet { public GUIContent uiName; public GUIContent uiLongName; public GUIContent uiDescription; public GUIContent helpIcon; /// /// Stores the previous known value of isEnabled as of the last call to `SetFeaturesFromEnabledFeatureSets` /// public bool wasEnabled; } static Dictionary> s_AllFeatureSets = null; struct FeatureSetState { public HashSet featureSetFeatureIds; public HashSet requiredToEnabledFeatureIds; public HashSet requiredToDisabledFeatureIds; public HashSet defaultToEnabledFeatureIds; } static Dictionary s_FeatureSetState = new Dictionary(); /// /// Event called when the feature set state has been changed. /// internal static event Action onFeatureSetStateChanged; /// /// The current active build target. Used to handle callbacks from into /// to determine if a feature can currently be disabled. /// public static BuildTargetGroup activeBuildTarget = BuildTargetGroup.Unknown; static void FillKnownFeatureSets(bool addTestFeatureSet = false) { BuildTargetGroup[] buildTargetGroups = new BuildTargetGroup[] { BuildTargetGroup.Standalone, BuildTargetGroup.WSA, BuildTargetGroup.Android }; if (addTestFeatureSet) { foreach (var buildTargetGroup in buildTargetGroups) { List knownFeatureSets = new List(); if (addTestFeatureSet) { knownFeatureSets.Add(new FeatureSetInfo() { isEnabled = false, name = "Known Test", featureSetId = "com.unity.xr.test.featureset", description = "Known Test feature group.", downloadText = "Click here to go to the Unity main website.", downloadLink = Constants.k_DocumentationURL, uiName = new GUIContent("Known Test"), uiDescription = new GUIContent("Known Test feature group."), helpIcon = new GUIContent("", CommonContent.k_HelpIcon.image, "Click here to go to the Unity main website."), }); } s_AllFeatureSets.Add(buildTargetGroup, knownFeatureSets); } } foreach (var kvp in KnownFeatureSets.k_KnownFeatureSets) { List knownFeatureSets; if (!s_AllFeatureSets.TryGetValue(kvp.Key, out knownFeatureSets)) { knownFeatureSets = new List(); foreach (var featureSet in kvp.Value) { knownFeatureSets.Add(new FeatureSetInfo() { isEnabled = false, name = featureSet.name, featureSetId = featureSet.featureSetId, description = featureSet.description, downloadText = featureSet.downloadText, downloadLink = featureSet.downloadLink, uiName = new GUIContent(featureSet.name), uiLongName = new GUIContent($"{featureSet.name} feature group"), uiDescription = new GUIContent(featureSet.description), helpIcon = new GUIContent("", CommonContent.k_HelpIcon.image, featureSet.downloadText), }); } s_AllFeatureSets.Add(kvp.Key, knownFeatureSets); } } } /// /// Initializes all currently known feature sets. This will do two initialization passes: /// /// 1) Starts with all built in/known feature sets. /// 2) Queries the system for anything with an /// defined on it and uses that to add/update the store of known feature sets. /// public static void InitializeFeatureSets() { InitializeFeatureSets(false); } internal static void InitializeFeatureSets(bool addTestFeatureSet) { if (s_AllFeatureSets == null) s_AllFeatureSets = new Dictionary>(); s_AllFeatureSets.Clear(); FillKnownFeatureSets(addTestFeatureSet); var types = TypeCache.GetTypesWithAttribute(); foreach (var t in types) { var attrs = Attribute.GetCustomAttributes(t); foreach (var attr in attrs) { var featureSetAttr = attr as OpenXRFeatureSetAttribute; if (featureSetAttr == null) continue; if (!addTestFeatureSet && featureSetAttr.FeatureSetId.Contains("com.unity.xr.test.featureset")) continue; foreach (var buildTargetGroup in featureSetAttr.SupportedBuildTargets) { var key = buildTargetGroup; if (!s_AllFeatureSets.ContainsKey(key)) { s_AllFeatureSets.Add(key, new List()); } var isEnabled = OpenXREditorSettings.Instance.IsFeatureSetSelected(buildTargetGroup, featureSetAttr.FeatureSetId); var newFeatureSet = new FeatureSetInfo() { isEnabled = isEnabled, wasEnabled = isEnabled, name = featureSetAttr.UiName, description = featureSetAttr.Description, featureSetId = featureSetAttr.FeatureSetId, downloadText = "", downloadLink = "", featureIds = featureSetAttr.FeatureIds, requiredFeatureIds = featureSetAttr.RequiredFeatureIds, defaultFeatureIds = featureSetAttr.DefaultFeatureIds, isInstalled = true, uiName = new GUIContent(featureSetAttr.UiName), uiLongName = new GUIContent($"{featureSetAttr.UiName} feature group"), uiDescription = new GUIContent(featureSetAttr.Description), helpIcon = String.IsNullOrEmpty(featureSetAttr.Description) ? null : new GUIContent("", CommonContent.k_HelpIcon.image, featureSetAttr.Description), }; bool foundFeatureSet = false; var featureSets = s_AllFeatureSets[key]; for (int i = 0; i < featureSets.Count; i++) { if (String.Compare(featureSets[i].featureSetId, newFeatureSet.featureSetId, true) == 0) { foundFeatureSet = true; featureSets[i] = newFeatureSet; break; } } if (!foundFeatureSet) featureSets.Add(newFeatureSet); } } } var buildTargetGroups = Enum.GetValues(typeof(BuildTargetGroup)); foreach (BuildTargetGroup buildTargetGroup in buildTargetGroups) { FeatureSetState fsi; if (!s_FeatureSetState.TryGetValue(buildTargetGroup, out fsi)) { fsi = new FeatureSetState(); fsi.featureSetFeatureIds = new HashSet(); fsi.requiredToEnabledFeatureIds = new HashSet(); fsi.requiredToDisabledFeatureIds = new HashSet(); fsi.defaultToEnabledFeatureIds = new HashSet(); s_FeatureSetState.Add(buildTargetGroup, fsi); } SetFeaturesFromEnabledFeatureSets(buildTargetGroup); } } /// /// Returns the list of all for the given build target group. /// /// The build target group to find the feature sets for. /// List of or null if there is nothing that matches the given input. public static List FeatureSetsForBuildTarget(BuildTargetGroup buildTargetGroup) { return OpenXRFeatureSetManager.FeatureSetInfosForBuildTarget(buildTargetGroup).Select((fi) => fi as FeatureSet).ToList(); } internal static List FeatureSetInfosForBuildTarget(BuildTargetGroup buildTargetGroup) { List ret = new List(); HashSet featureSetsForBuildTargetGroup = new HashSet(); if (s_AllFeatureSets == null) InitializeFeatureSets(); if (s_AllFeatureSets == null) return ret; foreach (var key in s_AllFeatureSets.Keys) { if (key == buildTargetGroup) { featureSetsForBuildTargetGroup.UnionWith(s_AllFeatureSets[key]); } } ret.AddRange(featureSetsForBuildTargetGroup); return ret; } /// /// Returns a specific instance that matches the input. /// /// The build target group this feature set supports. /// The feature set id for the specific feature set being requested. /// The matching or null. public static FeatureSet GetFeatureSetWithId(BuildTargetGroup buildTargetGroup, string featureSetId) { return GetFeatureSetInfoWithId(buildTargetGroup, featureSetId) as FeatureSet; } internal static FeatureSetInfo GetFeatureSetInfoWithId(BuildTargetGroup buildTargetGroup, string featureSetId) { var featureSets = FeatureSetInfosForBuildTarget(buildTargetGroup); if (featureSets != null) { foreach (var featureSet in featureSets) { if (String.Compare(featureSet.featureSetId, featureSetId, true) == 0) return featureSet; } } return null; } /// /// Given the current enabled state of the feature sets that match for a build target group, enable and disable the features associated with /// each feature set. Features that overlap sets of varying enabled states will maintain their enabled setting. /// /// The build target group to process features sets for. public static void SetFeaturesFromEnabledFeatureSets(BuildTargetGroup buildTargetGroup) { var extInfo = FeatureHelpersInternal.GetAllFeatureInfo(buildTargetGroup); SetFeaturesFromEnabledFeatureSets(buildTargetGroup, extInfo); } internal static void SetFeaturesFromEnabledFeatureSets(BuildTargetGroup buildTargetGroup, FeatureHelpersInternal.AllFeatureInfo extInfo) { var featureSets = FeatureSetInfosForBuildTarget(buildTargetGroup); var fsi = s_FeatureSetState[buildTargetGroup]; fsi.featureSetFeatureIds.Clear(); foreach (var featureSet in featureSets) { if (featureSet.featureIds != null) fsi.featureSetFeatureIds.UnionWith(featureSet.featureIds); } fsi.featureSetFeatureIds.Clear(); fsi.requiredToEnabledFeatureIds.Clear(); fsi.requiredToDisabledFeatureIds.Clear(); fsi.defaultToEnabledFeatureIds.Clear(); // Update the selected feature set states first foreach (var featureSet in featureSets) { if (featureSet.featureIds == null) continue; OpenXREditorSettings.Instance.SetFeatureSetSelected(buildTargetGroup, featureSet.featureSetId, featureSet.isEnabled); } foreach (var featureSet in featureSets) { if (featureSet.featureIds == null) continue; if (featureSet.isEnabled && featureSet.requiredFeatureIds != null) { fsi.requiredToEnabledFeatureIds.UnionWith(featureSet.requiredFeatureIds); } if (featureSet.isEnabled != featureSet.wasEnabled) { if (featureSet.isEnabled && featureSet.defaultFeatureIds != null) { fsi.defaultToEnabledFeatureIds.UnionWith(featureSet.defaultFeatureIds); } else if (!featureSet.isEnabled && featureSet.requiredFeatureIds != null) { fsi.requiredToDisabledFeatureIds.UnionWith(featureSet.requiredFeatureIds); } featureSet.wasEnabled = featureSet.isEnabled; } } foreach (var ext in extInfo.Features) { if (ext.Feature.enabled && fsi.requiredToDisabledFeatureIds.Contains(ext.Attribute.FeatureId)) ext.Feature.enabled = false; if (!ext.Feature.enabled && fsi.requiredToEnabledFeatureIds.Contains(ext.Attribute.FeatureId)) ext.Feature.enabled = true; if (!ext.Feature.enabled && fsi.defaultToEnabledFeatureIds.Contains(ext.Attribute.FeatureId)) ext.Feature.enabled = true; } s_FeatureSetState[buildTargetGroup] = fsi; onFeatureSetStateChanged?.Invoke(buildTargetGroup); } /// /// Tell the user if the feature with passed in feature id can be disabled. /// /// Uses the currently set to determine /// the BuildTargetGroup to use for checking against. If this value is not set, or /// set for a different build target than expected then return value may be incorrect. /// /// The feature id of the feature to check. /// True if currently required by some feature set, false otherwise. internal static bool CanFeatureBeDisabled(string featureId) { return CanFeatureBeDisabled(featureId, activeBuildTarget); } /// /// Tell the user if the feature with passed in feature id can be disabled based on the build target group. /// /// The feature id of the feature to check. /// The build target group whose feature sets you are checking against. /// True if currently required by some feature set, false otherwise. public static bool CanFeatureBeDisabled(string featureId, BuildTargetGroup buildTargetGroup) { if (!s_FeatureSetState.ContainsKey(buildTargetGroup)) return true; var fsi = s_FeatureSetState[buildTargetGroup]; return !fsi.requiredToEnabledFeatureIds.Contains(featureId); } internal static bool IsKnownFeatureSet(BuildTargetGroup buildTargetGroup, string featureSetId) { if (!KnownFeatureSets.k_KnownFeatureSets.ContainsKey(buildTargetGroup)) return false; var featureSets = KnownFeatureSets.k_KnownFeatureSets[buildTargetGroup]. Where((fs) => fs.featureSetId == featureSetId); return featureSets.Any(); } } }