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();
}
}
}