//----------------------------------------------------------------------- // // Copyright (c) Sirenix ApS. All rights reserved. // //----------------------------------------------------------------------- #if !SIRENIX_INTERNAL #pragma warning disable #endif using System; using Sirenix.OdinInspector; [assembly: RegisterAssetReferenceAttributeForwardToChild(typeof(InlineEditorAttribute))] [assembly: RegisterAssetReferenceAttributeForwardToChild(typeof(PreviewFieldAttribute))] namespace Sirenix.OdinInspector { using System.Diagnostics; /// /// DisallowAddressableSubAssetField is used on AssetReference properties, and disallows and prevents assigned sub-assets to the asset reference. /// /// /// /// [DisallowAddressableSubAssetField] /// public AssetReference Reference; /// /// /// /// [Conditional("UNITY_EDITOR")] [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.ReturnValue | AttributeTargets.Parameter)] public class DisallowAddressableSubAssetFieldAttribute : Attribute { } /// /// Registers an attribute to be applied to an AssetRefenece property, to be forwarded and applied to the AssetReference's child instead. /// This allows attributes designed for use on UnityEngine.Objects to be used on AssetReference properties as well. /// By default, InlineEditorAttribute and PreviewFieldAttribute are registered for forwarding. /// /// /// /// [assembly: Sirenix.OdinInspector.Modules.RegisterAssetReferenceAttributeForwardToChild(typeof(InlineEditorAttribute))] /// [assembly: Sirenix.OdinInspector.Modules.RegisterAssetReferenceAttributeForwardToChild(typeof(PreviewFieldAttribute))] /// /// /// [Conditional("UNITY_EDITOR")] [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] public class RegisterAssetReferenceAttributeForwardToChildAttribute : Attribute // TODO: Should this be a global attribute? { /// /// The type of the attribute to forward. /// public readonly Type AttributeType; /// /// Registers the specified attribute to be copied and applied to the AssetReference's UnityEngine.Object child instead. /// /// The attribute type to forward. public RegisterAssetReferenceAttributeForwardToChildAttribute(Type attributeType) { this.AttributeType = attributeType; } } } #if UNITY_EDITOR namespace Sirenix.OdinInspector.Modules.Addressables.Editor { using Sirenix.OdinInspector.Editor; using Sirenix.Serialization; using Sirenix.Utilities; using Sirenix.Utilities.Editor; using Sirenix.OdinInspector.Modules.Addressables.Editor.Internal; using Sirenix.Reflection.Editor; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEditor; using UnityEditor.AddressableAssets; using UnityEditor.AddressableAssets.Settings; using UnityEditor.AddressableAssets.GUI; using UnityEngine; using UnityEngine.AddressableAssets; using System.Runtime.Serialization; using UnityEngine.U2D; using UnityEditor.U2D; using System.IO; /// /// Draws an AssetReference property. /// /// The concrete type of AssetReference to be drawn. For example, AssetReferenceTexture. [DrawerPriority(0, 1, 0)] public class AssetReferenceDrawer : OdinValueDrawer, IDefinesGenericMenuItems where T : AssetReference { private bool hideAssetReferenceField; private Type[] validMainAssetTypes; private Type targetType; private bool targetTypeIsNotValidMainAsset; private string NoneSelectedLabel; //private string[] labelRestrictions; private bool showSubAssetField; private bool updateShowSubAssetField; private bool disallowSubAssets_Backing; private bool ActuallyDisallowSubAssets => this.disallowSubAssets_Backing && !this.targetTypeIsNotValidMainAsset; private List restrictions; private bool isSpriteAtlas; protected override bool CanDrawValueProperty(InspectorProperty property) { return property.GetAttribute() == null; } protected override void Initialize() { // If a child exists, we draw that child instead of the AssetReference field. if (this.Property.Children.Count > 0) { this.hideAssetReferenceField = true; return; } this.EnsureNotRealNull(); this.validMainAssetTypes = OdinAddressableUtility.GetAssetReferenceValidMainAssetTypes(typeof(T)); this.targetType = OdinAddressableUtility.GetAssetReferenceTargetType(typeof(T)); this.targetTypeIsNotValidMainAsset = this.validMainAssetTypes.Contains(this.targetType) == false; this.isSpriteAtlas = this.validMainAssetTypes.Length > 0 && this.validMainAssetTypes[0] == typeof(SpriteAtlas); if (this.targetType == typeof(UnityEngine.Object)) { this.NoneSelectedLabel = "None (Addressable Asset)"; } else if (this.validMainAssetTypes.Length > 1 || this.validMainAssetTypes[0] != this.targetType) { this.NoneSelectedLabel = $"None (Addressable [{string.Join("/", this.validMainAssetTypes.Select(n => n.GetNiceName()))}]>{this.targetType.GetNiceName()})"; } else { this.NoneSelectedLabel = $"None (Addressable {this.targetType.GetNiceName()})"; } this.restrictions = new List(); foreach (var attr in this.Property.Attributes) { if (attr is AssetReferenceUIRestriction r) { this.restrictions.Add(r); } } this.disallowSubAssets_Backing = Property.GetAttribute() != null; this.updateShowSubAssetField = true; } private string lastGuid; protected override void DrawPropertyLayout(GUIContent label) { if (this.disallowSubAssets_Backing && this.targetTypeIsNotValidMainAsset) { SirenixEditorGUI.WarningMessageBox($"This {typeof(T).GetNiceName()} field has been marked as not allowing sub assets, but the target type '{this.targetType.GetNiceName()}' is not a valid main asset for {typeof(T).GetNiceName()}, so the target value *must* be a sub asset. Therefore sub assets have been enabled. (Valid main asset types for {typeof(T).GetNiceName()} are: {string.Join(", ", this.validMainAssetTypes.Select(t => t.GetNiceName()))})"); } if (this.hideAssetReferenceField == false) { var value = ValueEntry.SmartValue; if (this.lastGuid != this.ValueEntry.SmartValue?.AssetGUID) { this.updateShowSubAssetField = true; } this.lastGuid = this.ValueEntry.SmartValue?.AssetGUID; // Update showSubAssetField. if (this.updateShowSubAssetField && Event.current.type == EventType.Layout) { if (value == null || value.AssetGUID == null || value.editorAsset == null) { this.showSubAssetField = false; } else if (string.IsNullOrEmpty(value.SubObjectName) == false) { this.showSubAssetField = true; } else if (this.ActuallyDisallowSubAssets) { this.showSubAssetField = false; } else { var path = AssetDatabase.GUIDToAssetPath(value.AssetGUID); if (path == null) { this.showSubAssetField = false; } else { var mainAsset = AssetDatabase.LoadMainAssetAtPath(path); this.showSubAssetField = OdinAddressableUtility.EnumerateAllActualAndVirtualSubAssets(mainAsset, path).Any(); } } this.updateShowSubAssetField = false; } var rect = SirenixEditorGUI.GetFeatureRichControlRect(label, out var controlId, out var _, out var valueRect); Rect mainRect = valueRect; Rect subRect = default, subPickerRect = default; if (this.showSubAssetField) { subRect = mainRect.Split(1, 2).AddX(1); mainRect = mainRect.Split(0, 2).SubXMax(1); subPickerRect = subRect.AlignRight(16); } var mainPickerRect = mainRect.AlignRight(16); // Cursor EditorGUIUtility.AddCursorRect(mainPickerRect, MouseCursor.Link); if (showSubAssetField) { EditorGUIUtility.AddCursorRect(subPickerRect, MouseCursor.Link); } // Selector if (GUI.Button(mainPickerRect, "", SirenixGUIStyles.None)) { OpenMainAssetSelector(valueRect); } if (showSubAssetField && GUI.Button(subPickerRect, "", SirenixGUIStyles.None)) { OpenSubAssetSelector(valueRect); } // Ping if (Event.current.type == EventType.MouseDown && Event.current.button == 0 && mainRect.Contains(Event.current.mousePosition) && value != null && value.editorAsset != null) { EditorGUIUtility.PingObject(value.editorAsset); } // Drag and drop EditorGUI.BeginChangeCheck(); var drop = DragAndDropUtilities.DropZone(rect, null, typeof(object), false, controlId); if (EditorGUI.EndChangeCheck()) { this.EnsureNotRealNull(); if (this.ConvertToValidAssignment(drop, out Object obj, out bool isSubAssetAssignment)) { if (this.isSpriteAtlas && obj is Sprite sprite) { foreach (SpriteAtlas spriteAtlas in AssetDatabase_Internals.FindAssets(String.Empty, false, AssetDatabaseSearchArea.AllAssets)) { if (!spriteAtlas.CanBindTo(sprite)) { continue; } this.SetMainAndSubAsset(spriteAtlas, sprite); break; } } else { if (isSubAssetAssignment) { string path = AssetDatabase.GetAssetPath(obj); UnityEngine.Object mainAsset = AssetDatabase.LoadMainAssetAtPath(path); if (mainAsset != null) { if (mainAsset is Sprite mainAssetSprite) { this.SetMainAndSubAsset(mainAssetSprite, obj); } else { this.SetMainAndSubAsset(mainAsset, obj); } } this.updateShowSubAssetField = true; } else { var isSet = false; if (string.IsNullOrEmpty(this.ValueEntry.SmartValue.SubObjectName)) { if (obj is Sprite) { Object[] subAssets = AssetDatabase.LoadAllAssetRepresentationsAtPath(AssetDatabase.GetAssetPath(obj)); if (subAssets.Length > 0) { this.SetMainAndSubAsset(obj, subAssets[0]); isSet = true; } } } if (!isSet) { this.SetMainAsset(obj); } } } if (this.ActuallyDisallowSubAssets && !this.targetTypeIsNotValidMainAsset && !string.IsNullOrEmpty(this.ValueEntry.SmartValue.SubObjectName)) { this.SetSubAsset(null); } } else if (drop == null) { this.SetMainAsset(null); } } // Drawing if (Event.current.type == EventType.Repaint) { GUIContent valueLabel; if (value == null || string.IsNullOrEmpty(value.AssetGUID) || value.editorAsset == null) { valueLabel = GUIHelper.TempContent(NoneSelectedLabel); } else if (showSubAssetField) { var path = AssetDatabase.GUIDToAssetPath(value.AssetGUID); var assetName = System.IO.Path.GetFileNameWithoutExtension(path); valueLabel = GUIHelper.TempContent(assetName, GetTheDamnPreview(value.editorAsset)); } else { valueLabel = GUIHelper.TempContent(value.editorAsset.name, GetTheDamnPreview(value.editorAsset)); } GUI.Label(mainRect, valueLabel, EditorStyles.objectField); SdfIcons.DrawIcon(mainPickerRect.SetWidth(12), SdfIconType.Record2); if (this.showSubAssetField) { if (string.IsNullOrEmpty(value.SubObjectName) || value.editorAsset == null) { valueLabel = GUIHelper.TempContent(""); } else { valueLabel = GUIHelper.TempContent(value.SubObjectName); } GUI.Label(subRect, valueLabel, EditorStyles.objectField); SdfIcons.DrawIcon(subPickerRect.SetWidth(12), SdfIconType.Record2); } } } else { this.Property.Children[0].Draw(label); } } private static Texture2D GetTheDamnPreview(UnityEngine.Object obj) { Texture2D img = obj as Texture2D; if (img == null) { img = (obj as Sprite)?.texture; } if (img == null) { img = AssetPreview.GetMiniThumbnail(obj); } return img; } private bool ConvertToValidAssignment(object drop, out UnityEngine.Object converted, out bool isSubAssetAssignment) { converted = null; isSubAssetAssignment = false; bool isDefinitelyMainAssetAssignment = false; if (object.ReferenceEquals(drop, null)) return false; if (!ConvertUtility.TryWeakConvert(drop, this.targetType, out object convertedObj)) { for (int i = 0; i < this.validMainAssetTypes.Length; i++) { if (ConvertUtility.TryWeakConvert(drop, this.validMainAssetTypes[i], out convertedObj)) { isDefinitelyMainAssetAssignment = true; break; } } } if (convertedObj == null || !(convertedObj is UnityEngine.Object unityObj) || unityObj == null) return false; converted = unityObj; if (isDefinitelyMainAssetAssignment) { isSubAssetAssignment = false; return true; } else if (AssetDatabase.IsSubAsset(converted)) { if (this.ActuallyDisallowSubAssets) { return false; } isSubAssetAssignment = true; return true; } return true; } private void OpenMainAssetSelector(Rect rect) { this.EnsureNotRealNull(); var selector = new AddressableSelector("Select", this.validMainAssetTypes, this.restrictions, typeof(T)); bool isUnityRoot = this.Property.SerializationRoot?.ValueEntry.WeakSmartValue is UnityEngine.Object; if (isUnityRoot) { Undo.IncrementCurrentGroup(); int undoIndex = Undo.GetCurrentGroup(); selector.SelectionCancelled += () => { Undo.RevertAllDownToGroup(undoIndex); }; selector.SelectionConfirmed += entries => { Undo.RevertAllDownToGroup(undoIndex); this.OnMainAssetSelect(entries.FirstOrDefault()); }; } else { selector.SelectionConfirmed += entries => { this.OnMainAssetSelect(entries.FirstOrDefault()); }; } selector.SelectionChangedWithType += (type, entries) => { if (type == SelectionChangedType.SelectionCleared) { return; } AddressableAssetEntry entry = entries.FirstOrDefault(); this.OnMainAssetSelect(entry); }; selector.ShowInPopup(rect); } private void OpenSubAssetSelector(Rect rect) { this.EnsureNotRealNull(); if (this.ValueEntry.SmartValue == null || this.ValueEntry.SmartValue.AssetGUID == null) return; var path = AssetDatabase.GUIDToAssetPath(this.ValueEntry.SmartValue.AssetGUID); if (path == null) return; var mainAsset = AssetDatabase.LoadMainAssetAtPath(path); List subAssets; if (mainAsset != null && mainAsset is SpriteAtlas) { subAssets = OdinAddressableUtility.EnumerateAllActualAndVirtualSubAssets(mainAsset, path) .Where(val => val != null && (val is Sprite || val is Texture2D)) .ToList(); } else { subAssets = OdinAddressableUtility.EnumerateAllActualAndVirtualSubAssets(mainAsset, path) .Where(val => val != null && this.targetType.IsInstanceOfType(val)) .ToList(); } var items = new GenericSelectorItem[subAssets.Count + 1]; items[0] = new GenericSelectorItem("", null); for (int i = 0; i < subAssets.Count; i++) { var item = new GenericSelectorItem(subAssets[i].name, subAssets[i]); items[i + 1] = item; } var selector = new GenericSelector("Select Sub Asset", false, items); selector.SelectionChanged += OnSubAssetSelect; selector.SelectionConfirmed += OnSubAssetSelect; selector.ShowInPopup(rect); } private void OnMainAssetSelect(AddressableAssetEntry entry) => this.UpdateAssetReference(entry); private void OnSubAssetSelect(IEnumerable selection) { if (this.ValueEntry == null || this.ValueEntry.SmartValue.AssetGUID == null) { return; } UnityEngine.Object selected = selection.FirstOrDefault(); this.SetSubAsset(selected); } private void UpdateAssetReference(AddressableAssetEntry entry) { if (entry == null) { this.SetMainAsset(null); return; } if (typeof(T).InheritsFrom()) { this.SetMainAsset(entry.MainAsset); return; } if (typeof(T).InheritsFrom()) { UnityEngine.Object subObject = null; string path = AssetDatabase.GetAssetPath(entry.TargetAsset); if (AssetDatabase.GetMainAssetTypeAtPath(path) == typeof(SpriteAtlas)) { if (!(entry.TargetAsset is SpriteAtlas)) { subObject = entry.TargetAsset; } } this.SetMainAndSubAsset(entry.MainAsset, subObject); } else if (!this.ActuallyDisallowSubAssets && AssetDatabase.IsSubAsset(entry.TargetAsset)) { this.SetMainAndSubAsset(entry.MainAsset, entry.TargetAsset); } else { this.SetMainAsset(entry.MainAsset); } } private T CreateAssetReferenceFrom(AddressableAssetEntry entry) { if (entry != null) { return CreateAssetReferenceFrom(entry.TargetAsset); } else { return null; } } private T CreateAssetReferenceFrom(UnityEngine.Object mainAsset, UnityEngine.Object subAsset) { var path = AssetDatabase.GetAssetPath(mainAsset); var guid = AssetDatabase.AssetPathToGUID(path); if (guid == null) return null; var instance = (T)Activator.CreateInstance(typeof(T), guid); instance.SetEditorSubObject(subAsset); return instance; } private T CreateAssetReferenceFrom(UnityEngine.Object obj) { var path = AssetDatabase.GetAssetPath(obj); var guid = AssetDatabase.AssetPathToGUID(path); if (guid == null) return null; var instance = (T)Activator.CreateInstance(typeof(T), guid); if (typeof(T).InheritsFrom()) { if (AssetDatabase.GetMainAssetTypeAtPath(path) == typeof(SpriteAtlas)) { if (!(obj is SpriteAtlas)) { instance.SetEditorSubObject(obj); } } } else if (typeof(T).InheritsFrom()) { // No need to do anything here. // The user will need to choose a sprite // "sub asset" from the atlas. } else if (this.ActuallyDisallowSubAssets == false && AssetDatabase.IsSubAsset(obj)) { instance.SetEditorSubObject(obj); } return instance; } public void PopulateGenericMenu(InspectorProperty property, GenericMenu genericMenu) { genericMenu.AddItem(new GUIContent("Set To Null"), false, () => { this.EnsureNotRealNull(); this.SetMainAsset(null); }); if (this.ValueEntry.SmartValue != null && string.IsNullOrEmpty(this.ValueEntry.SmartValue.SubObjectName) == false) { genericMenu.AddItem(new GUIContent("Remove Sub Asset"), false, () => { this.EnsureNotRealNull(); this.SetSubAsset(null); }); } else { genericMenu.AddDisabledItem(new GUIContent("Remove Sub Asset")); } genericMenu.AddItem(new GUIContent("Open Groups Window"), false, OdinAddressableUtility.OpenGroupsWindow); } private void SetMainAndSubAsset(UnityEngine.Object mainAsset, UnityEngine.Object subAsset, bool setDirtyIfChanged = true) { string subAssetName = subAsset == null ? null : subAsset.name; bool isDifferent = this.ValueEntry.SmartValue.editorAsset != mainAsset || this.ValueEntry.SmartValue.SubObjectName != subAssetName; if (!isDifferent) { return; } if (this.Property.SerializationRoot?.ValueEntry.WeakSmartValue is UnityEngine.Object) { Undo.IncrementCurrentGroup(); Undo.SetCurrentGroupName("Main- and Sub Asset Changed"); int index = Undo.GetCurrentGroup(); this.SetMainAsset(mainAsset, false); this.SetSubAsset(subAsset, false); Undo.CollapseUndoOperations(index); if (setDirtyIfChanged) { this.Property.MarkSerializationRootDirty(); } } else { this.SetMainAsset(mainAsset, false); this.SetSubAsset(subAsset, false); } } private void SetMainAsset(UnityEngine.Object asset, bool setDirtyIfChanged = true) { if (this.ValueEntry.SmartValue.editorAsset == asset) { return; } this.Property.RecordForUndo("Main Asset Changed"); this.ValueEntry.SmartValue.SetEditorAsset(asset); this.updateShowSubAssetField = true; if (setDirtyIfChanged) { this.Property.MarkSerializationRootDirty(); } } private void SetSubAsset(UnityEngine.Object asset, bool setDirtyIfChanged = true) { #if SIRENIX_INTERNAL if (this.ValueEntry.SmartValue.editorAsset == null) { Debug.LogError("[SIRENIX INTERNAL] Attempted to assign the Sub Asset on an AssetReference without the Main Asset being assigned first."); return; } #endif string assetName = asset == null ? null : asset.name; if (this.ValueEntry.SmartValue.SubObjectName == assetName) { return; } this.Property.RecordForUndo("Sub Asset Changed"); this.ValueEntry.SmartValue.SetEditorSubObject(asset); this.updateShowSubAssetField = true; if (setDirtyIfChanged) { this.Property.MarkSerializationRootDirty(); } } private void EnsureNotRealNull() { if (this.ValueEntry.WeakSmartValue == null) { this.ValueEntry.SmartValue = OdinAddressableUtility.CreateAssetReferenceGuid(null); } } } /// /// Draws an AssetLabelReference field. /// [DrawerPriority(0, 1, 0)] public class AssetLabelReferenceDrawer : OdinValueDrawer, IDefinesGenericMenuItems { protected override bool CanDrawValueProperty(InspectorProperty property) { return property.GetAttribute() == null; } protected override void DrawPropertyLayout(GUIContent label) { var rect = SirenixEditorGUI.GetFeatureRichControlRect(label, out var controlId, out var hasKeyboardFocus, out var valueRect); string valueLabel; if (this.ValueEntry.SmartValue == null || string.IsNullOrEmpty(this.ValueEntry.SmartValue.labelString)) { valueLabel = ""; } else { valueLabel = this.ValueEntry.SmartValue.labelString; } if (GUI.Button(valueRect, valueLabel, EditorStyles.popup)) { var selector = new AddressableLabelSelector(); selector.SelectionChanged += SetLabel; selector.SelectionConfirmed += SetLabel; selector.ShowInPopup(valueRect); } } private void SetLabel(IEnumerable selection) { var selected = selection.FirstOrDefault(); this.ValueEntry.SmartValue = new AssetLabelReference() { labelString = selected, }; } public void PopulateGenericMenu(InspectorProperty property, GenericMenu genericMenu) { genericMenu.AddItem(new GUIContent("Set To Null"), false, () => property.ValueEntry.WeakSmartValue = null); genericMenu.AddItem(new GUIContent("Open Label Window"), false, () => OdinAddressableUtility.OpenLabelsWindow()); } } /// /// Odin Selector for Addressables. /// public class AddressableSelector : OdinSelector { //private static EditorPrefBool flatten = new EditorPrefBool("AddressablesSelector.Flatten", false); public event Action> SelectionChangedWithType; private static EditorPrefEnum listMode = new EditorPrefEnum("AddressablesSelector.ListMode", SelectorListMode.Group); private readonly string title; private readonly Type[] filterTypes; private readonly List restrictions; internal bool ShowNonAddressables; public override string Title => this.title; /// /// Initializes a AddressableSelector. /// /// The title of the selector. Set to null for no title. /// The type of UnityEngine.Object to be selectable. For example, UnityEngine.Texture. For no restriction, pass in UnityEngine.Object. /// The Addressable labels to restrict the selector to. Set to null for no label restrictions. /// Throws if the filter type is null. public AddressableSelector(string title, Type filterType, List restrictions, Type assetReferenceType) : this(title, new Type[] { filterType }, restrictions, assetReferenceType) { } /// /// Initializes a AddressableSelector. /// /// The title of the selector. Set to null for no title. /// The types of UnityEngine.Object to be selectable. For example, UnityEngine.Texture. For no restriction, pass in an array containing UnityEngine.Object. /// The Addressable labels to restrict the selector to. Set to null for no label restrictions. /// Throws if the filter type is null. public AddressableSelector(string title, Type[] filterTypes, List restrictions, Type assetReferenceType) { this.title = title; this.filterTypes = filterTypes ?? throw new ArgumentNullException(nameof(filterTypes)); this.restrictions = restrictions; if (assetReferenceType != null) { if (assetReferenceType.InheritsFrom() == false) { throw new ArgumentException("Must inherit AssetReference", nameof(assetReferenceType)); } else if (assetReferenceType.IsAbstract) { throw new ArgumentException("Cannot be abstract type.", nameof(assetReferenceType)); } } } protected override void DrawToolbar() { bool drawTitle = !string.IsNullOrEmpty(this.Title); bool drawSearchToolbar = this.SelectionTree.Config.DrawSearchToolbar; bool drawButton = this.DrawConfirmSelectionButton; if (drawTitle || drawSearchToolbar || drawButton) { SirenixEditorGUI.BeginHorizontalToolbar(this.SelectionTree.Config.SearchToolbarHeight); { DrawToolbarTitle(); DrawToolbarSearch(); EditorGUI.DrawRect(GUILayoutUtility.GetLastRect().AlignLeft(1), SirenixGUIStyles.BorderColor); SdfIconType icon; if (listMode.Value == SelectorListMode.Path) icon = SdfIconType.ListNested; else if (listMode.Value == SelectorListMode.Group) icon = SdfIconType.ListStars; else if (listMode.Value == SelectorListMode.Flat) icon = SdfIconType.List; else icon = SdfIconType.X; if (SirenixEditorGUI.ToolbarButton(icon, true)) { int m = (int)listMode.Value + 1; if (m >= (int)SelectorListMode.Max) { m = 0; } listMode.Value = (SelectorListMode)m; this.RebuildMenuTree(); } EditorGUI.BeginChangeCheck(); this.ShowNonAddressables = SirenixEditorGUI.ToolbarToggle(this.ShowNonAddressables, EditorIcons.UnityLogo); if (EditorGUI.EndChangeCheck()) { this.RebuildMenuTree(); } if (SirenixEditorGUI.ToolbarButton(SdfIconType.GearFill, true)) { OdinAddressableUtility.OpenGroupsWindow(); } DrawToolbarConfirmButton(); } SirenixEditorGUI.EndHorizontalToolbar(); } } protected override void BuildSelectionTree(OdinMenuTree tree) { if (this.SelectionChangedWithType != null) { tree.Selection.SelectionChanged += type => { IEnumerable selection = this.GetCurrentSelection(); if (this.IsValidSelection(selection)) { this.SelectionChangedWithType(type, selection); } }; } tree.Config.EXPERIMENTAL_INTERNAL_SparseFixedLayouting = true; tree.Config.SelectMenuItemsOnMouseDown = true; if (AddressableAssetSettingsDefaultObject.SettingsExists) { AddressableAssetSettings settings = AddressableAssetSettingsDefaultObject.Settings; foreach (AddressableAssetGroup group in settings.groups) { if (group == null || group.name == "Built In Data") { continue; } foreach (AddressableAssetEntry entry in group.entries) { this.AddEntriesToTree(tree, group.name, entry); } } } foreach (OdinMenuItem item in tree.EnumerateTree()) { if (item.Value == null) { item.SdfIcon = SdfIconType.Folder; } } if (this.ShowNonAddressables) { var searchFilter = ""; foreach (Type filterType in this.filterTypes) { searchFilter += $"t:{filterType.Name} "; } IEnumerator enumerator = AssetDatabase_Internals.EnumerateAllAssets(searchFilter, false, AssetDatabaseSearchArea.InAssetsOnly); if (enumerator.MoveNext()) { var addedGuids = new HashSet(); foreach (OdinMenuItem item in tree.EnumerateTree()) { if (item.Value != null) { addedGuids.Add((item.Value as AddressableAssetEntry).guid); } } const string NON_ADDRESSABLES_ITEM_NAME = "Non Addressables"; var nonAddressablesItem = new OdinMenuItem(tree, NON_ADDRESSABLES_ITEM_NAME, null) {Icon = EditorIcons.UnityLogo}; tree.MenuItems.Add(nonAddressablesItem); do { HierarchyProperty current = enumerator.Current; if (addedGuids.Contains(current.guid) || !current.isMainRepresentation) { continue; } AddressableAssetEntry entry = OdinAddressableUtility.CreateFakeAddressableAssetEntry(current.guid); if (listMode == SelectorListMode.Flat) { var item = new OdinMenuItem(tree, current.name, entry) {Icon = current.icon}; nonAddressablesItem.ChildMenuItems.Add(item); } else { string path = AssetDatabase.GetAssetPath(current.instanceID); if (!current.isFolder) { int extensionEndingIndex = GetExtensionsEndingIndex(path); if (extensionEndingIndex != -1) { path = path.Substring(0, extensionEndingIndex); } } path = RemoveBaseDirectoryFromAssetPath(path); tree.Add($"{NON_ADDRESSABLES_ITEM_NAME}/{path}", entry, current.icon); } } while (enumerator.MoveNext()); nonAddressablesItem.ChildMenuItems.SortMenuItemsByName(); } } OdinMenuItem noneItem; if (this.filterTypes.Contains(typeof(UnityEngine.Object))) { noneItem = new OdinMenuItem(tree, " (Addressable Asset)", null); } else { string filterTypesJoined; if (this.filterTypes.Length == 1) { filterTypesJoined = this.filterTypes[0].GetNiceName(); } else { filterTypesJoined = string.Join("/", this.filterTypes.Select(t => t.GetNiceName())); } noneItem = new OdinMenuItem(tree, $" (Addressable {filterTypesJoined})", null); } noneItem.SdfIcon = SdfIconType.X; tree.MenuItems.Insert(0, noneItem); } private static int GetExtensionsEndingIndex(string path) { for (var i = path.Length - 1; i >= 0; i--) { if (path[i] == '\\' || path[i] == '/') { return -1; } if (path[i] == '.') { return i; } } return -1; } private static string RemoveBaseDirectoryFromAssetPath(string path) { if (path.StartsWith("Assets/")) { return path.Remove(0, "Assets/".Length); } return path; } private void AddEntriesToTree(OdinMenuTree tree, string groupName, AddressableAssetEntry entry) { if (entry == null) { return; } bool isFolder = entry.IsFolder || AssetDatabase.IsValidFolder(entry.AssetPath); if (isFolder) { entry.GatherAllAssets(null, false, false, true, null); if (entry.SubAssets != null) { foreach (AddressableAssetEntry e in entry.SubAssets) { this.AddEntriesToTree(tree, groupName, e); } } } else { UnityEngine.Object asset = entry.TargetAsset; if (asset == null) { return; } Type assetType = asset.GetType(); var inheritsFromFilterType = false; for (var i = 0; i < this.filterTypes.Length; i++) { if (this.filterTypes[i].IsAssignableFrom(assetType)) { inheritsFromFilterType = true; break; } } if (inheritsFromFilterType && this.PassesRestrictions(entry)) { string name; if (listMode.Value == SelectorListMode.Group) { name = entry.address; } else if (listMode.Value == SelectorListMode.Path) { name = System.IO.Path.GetFileNameWithoutExtension(entry.AssetPath); } else if (listMode.Value == SelectorListMode.Flat) { name = entry.address; } else { throw new Exception("Unsupported list mode: " + listMode.Value); } var item = new OdinMenuItem(tree, name, entry) { Icon = AssetPreview.GetMiniThumbnail(asset) }; if (listMode.Value == SelectorListMode.Group) { OdinMenuItem groupItem = tree.GetMenuItem(groupName); if (groupItem == null) { groupItem = new OdinMenuItem(tree, groupName, null); tree.MenuItems.Add(groupItem); } if (entry.ParentEntry != null && entry.ParentEntry.IsFolder) { OdinMenuItem folderItem = null; for (int i = 0; i < groupItem.ChildMenuItems.Count; i++) { if (groupItem.ChildMenuItems[i].Name == entry.ParentEntry.address) { folderItem = groupItem.ChildMenuItems[i]; break; } } if (folderItem == null) { folderItem = new OdinMenuItem(tree, entry.ParentEntry.address, null); groupItem.ChildMenuItems.Add(folderItem); } folderItem.ChildMenuItems.Add(item); } else { groupItem.ChildMenuItems.Add(item); } } else if (listMode.Value == SelectorListMode.Path) { tree.AddMenuItemAtPath(System.IO.Path.GetDirectoryName(entry.AssetPath), item); } else if (listMode.Value == SelectorListMode.Flat) { tree.MenuItems.Add(item); } } } } private bool PassesRestrictions(AddressableAssetEntry entry) { if (restrictions == null) return true; return OdinAddressableUtility.ValidateAssetReferenceRestrictions(restrictions, entry.MainAsset); //for (int i = 0; i < this.restrictions.Count; i++) //{ // if (this.restrictions[i].ValidateAsset(entry.AssetPath) == false) // { // return false; // } //} //return true; /* If for whatever reason Unity haven't actually implemented their restriction methods, then we can use this code to atleast implement label restriction. */ //if (this.labelRestrictions == null) return true; //for (int i = 0; i < labelRestrictions.Length; i++) //{ // if (entry.labels.Contains(labelRestrictions[i])) return true; //} //return false; } private enum SelectorListMode { Group, Path, Flat, Max, } } public class AddressableLabelSelector : OdinSelector { protected override void DrawToolbar() { bool drawTitle = !string.IsNullOrEmpty(this.Title); bool drawSearchToolbar = this.SelectionTree.Config.DrawSearchToolbar; bool drawButton = this.DrawConfirmSelectionButton; if (drawTitle || drawSearchToolbar || drawButton) { SirenixEditorGUI.BeginHorizontalToolbar(this.SelectionTree.Config.SearchToolbarHeight); { DrawToolbarTitle(); DrawToolbarSearch(); EditorGUI.DrawRect(GUILayoutUtility.GetLastRect().AlignLeft(1), SirenixGUIStyles.BorderColor); if (SirenixEditorGUI.ToolbarButton(SdfIconType.GearFill, true)) { OdinAddressableUtility.OpenLabelsWindow(); } DrawToolbarConfirmButton(); } SirenixEditorGUI.EndHorizontalToolbar(); } } protected override void BuildSelectionTree(OdinMenuTree tree) { IList labels = null; if (AddressableAssetSettingsDefaultObject.SettingsExists) { var settings = AddressableAssetSettingsDefaultObject.Settings; labels = settings.GetLabels(); } if (labels == null) labels = Array.Empty(); tree.MenuItems.Add(new OdinMenuItem(tree, "", null)); for (int i = 0; i < labels.Count; i++) { tree.MenuItems.Add(new OdinMenuItem(tree, labels[i], labels[i])); } } } /// /// Resolves children for AssetReference properties, and implements the RegisterAssetReferenceAttributeForwardToChild behaviour. /// /// The concrete type of AssetReference to be drawn. For example, AssetReferenceTexture. public class AssetReferencePropertyResolver : OdinPropertyResolver where T : AssetReference { private static readonly Type[] attributesToForward; static AssetReferencePropertyResolver() { attributesToForward = AppDomain.CurrentDomain.GetAssemblies() .SelectMany(x => x.GetCustomAttributes()) .Cast() .Select(x => x.AttributeType) .ToArray(); } public override int ChildNameToIndex(string name) { return 0; } public override int ChildNameToIndex(ref StringSlice name) { return 0; } public override InspectorPropertyInfo GetChildInfo(int childIndex) { var targetType = OdinAddressableUtility.GetAssetReferenceTargetType(typeof(T)); var getterSetterType = typeof(AssetReferenceValueGetterSetter<>).MakeGenericType(typeof(T), targetType); var getterSetter = Activator.CreateInstance(getterSetterType) as IValueGetterSetter; List attributes = new List { new ShowInInspectorAttribute(), }; foreach (var type in attributesToForward) { var attr = this.Property.Attributes.FirstOrDefault(x => x.GetType() == type); if (attr != null) { attributes.Add(attr); } } string label = "Asset"; return InspectorPropertyInfo.CreateValue(label, 0, SerializationBackend.None, getterSetter, attributes); } protected override int GetChildCount(T value) { foreach (var attr in attributesToForward) { if (this.Property.Attributes.Any(x => x.GetType() == attr)) { return 1; } } return 0; } private class AssetReferenceValueGetterSetter : IValueGetterSetter where TTarget : UnityEngine.Object { public bool IsReadonly => false; public Type OwnerType => typeof(T); public Type ValueType => typeof(TTarget); public TTarget GetValue(ref T owner) { var v = owner.editorAsset; return v as TTarget; } public object GetValue(object owner) { var v = (owner as T)?.editorAsset; return v as TTarget; } public void SetValue(ref T owner, TTarget value) { owner.SetEditorAsset(value); } public void SetValue(object owner, object value) { (owner as T).SetEditorAsset(value as TTarget); } } } /// /// Processes attributes for AssetReference properties. /// /// The concrete type of AssetReference to be drawn. For example, AssetReferenceTexture. public class AssetReferenceAttributeProcessor : OdinAttributeProcessor where T : AssetReference { public override void ProcessSelfAttributes(InspectorProperty property, List attributes) { attributes.Add(new DoNotDrawAsReferenceAttribute()); attributes.Add(new HideReferenceObjectPickerAttribute()); attributes.Add(new SuppressInvalidAttributeErrorAttribute()); // TODO: Remove this with proper attribute forwarding support. } } /// /// Processes attributes for AssetLabelReference properties. /// public class AssetLabelReferenceAttributeProcessor : OdinAttributeProcessor { public override void ProcessSelfAttributes(InspectorProperty property, List attributes) { attributes.Add(new DoNotDrawAsReferenceAttribute()); attributes.Add(new HideReferenceObjectPickerAttribute()); } } /// /// Implements conversion behaviour for addressables. /// [InitializeOnLoad] internal class AssetReferenceConverter : ConvertUtility.ICustomConverter { private readonly Type type_AssetEntryTreeViewItem; private WeakValueGetter get_AssetEntryTreeViewItem_entry; static AssetReferenceConverter() { ConvertUtility.AddCustomConverter(new AssetReferenceConverter()); } public AssetReferenceConverter() { this.type_AssetEntryTreeViewItem = TwoWaySerializationBinder.Default.BindToType("UnityEditor.AddressableAssets.GUI.AssetEntryTreeViewItem") ?? throw new Exception("Failed to find UnityEditor.AddressableAssets.GUI.AddressableAssetEntryTreeViewItem type."); var field_AssetEntryTreeViewItem_entry = type_AssetEntryTreeViewItem.GetField("entry", Flags.AllMembers) ?? throw new Exception("Failed to find entry field in UnityEditor.AddressableAssets.GUI.AddressableAssetEntryTreeViewItem type."); this.get_AssetEntryTreeViewItem_entry = EmitUtilities.CreateWeakInstanceFieldGetter(type_AssetEntryTreeViewItem, field_AssetEntryTreeViewItem_entry); } // UnityEngine.Object > AssetReference/T // AddressableAssetEntry > AssetReference // AssetReference/T > UnityEngine.Object // AssetReference/T > AssetReference/T // AddressableAssetEntry > UnityEngine.Object public bool CanConvert(Type from, Type to) { var comparer = FastTypeComparer.Instance; if (to.InheritsFrom(typeof(AssetReference))) { if (comparer.Equals(from, typeof(AddressableAssetEntry)) || comparer.Equals(from, type_AssetEntryTreeViewItem)) { return true; } else if (from.InheritsFrom()) { if (to.InheritsFrom(typeof(AssetReferenceT<>))) { var baseType = to.GetGenericBaseType(typeof(AssetReferenceT<>)); var targetType = baseType.GetGenericArguments()[0]; return from.InheritsFrom(targetType); } else { return true; } } else if (from.InheritsFrom(typeof(AssetReference))) { return to.InheritsFrom(from); } else { return false; } } else if (from.InheritsFrom(typeof(AssetReference)) && to.InheritsFrom()) { return false; } else { return false; } } public bool TryConvert(object obj, Type to, out object result) { if (obj == null) { result = null; return false; } var comparer = FastTypeComparer.Instance; // AssetEntryTreeViewItems is a UI element container for AddressableAssetEntry. // With this we can just treat AssetEntryTreeViewItems as an AddressableAssetEntry. if (comparer.Equals(obj.GetType(), type_AssetEntryTreeViewItem)) { obj = get_AssetEntryTreeViewItem_entry(ref obj); } if (to.InheritsFrom(typeof(AssetReference))) { Type assetType; if (to.InheritsFrom(typeof(AssetReferenceT<>))) { var baseType = to.GetGenericBaseType(typeof(AssetReferenceT<>)); assetType = baseType.GetGenericArguments()[0]; } else { assetType = typeof(UnityEngine.Object); } if (obj is UnityEngine.Object uObj) { if (obj.GetType().InheritsFrom(assetType)) { string guid = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(uObj)); if (string.IsNullOrEmpty(guid) == false) { result = CreateReference(to, uObj); return true; } else { result = null; return false; } } else { result = null; return false; } } else if (obj is AddressableAssetEntry entry) { if (entry.TargetAsset.GetType().InheritsFrom(assetType)) { result = CreateReference(to, entry.TargetAsset); return true; } else { result = null; return false; } } else if (obj is AssetReference reference) { if (TryGetReferencedAsset(reference, assetType, out var asset)) { result = CreateReference(to, asset); return true; } else { result = null; return false; } } else { result = null; return false; } } else if (to.InheritsFrom(typeof(UnityEngine.Object)) && obj is AssetReference reference) { if (TryGetReferencedAsset(reference, to, out var asset)) { result = asset; return true; } else { result = null; return false; } } else if (to.InheritsFrom(typeof(UnityEngine.Object)) && obj is AddressableAssetEntry entry) { var target = entry.TargetAsset; if (target == null) { result = null; return false; } else if (target.GetType().InheritsFrom(to)) { result = target; return true; } else if (ConvertUtility.TryWeakConvert(target, to, out var converted)) { result = converted; return true; } else { result = null; return false; } } else { result = null; return false; } } private bool TryGetReferencedAsset(AssetReference reference, Type to, out UnityEngine.Object asset) { if (reference.AssetGUID == null) { asset = null; return false; } var path = AssetDatabase.GUIDToAssetPath(reference.AssetGUID); if (reference.SubObjectName != null) { asset = null; foreach (var subAsset in OdinAddressableUtility.EnumerateAllActualAndVirtualSubAssets(reference.editorAsset, path)) { if (subAsset.name == reference.SubObjectName) { asset = subAsset; break; } } } else { asset = AssetDatabase.LoadAssetAtPath(path); } if (asset != null) { if (asset.GetType().InheritsFrom(to)) { return true; } else if (ConvertUtility.TryWeakConvert(asset, to, out var converted)) { asset = (UnityEngine.Object)converted; return true; } else { asset = null; return false; } } else { return false; } } private AssetReference CreateReference(Type type, UnityEngine.Object obj) { var reference = (AssetReference)Activator.CreateInstance(type, new string[] { AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(obj)) }); if (AssetDatabase.IsSubAsset(obj)) { reference.SetEditorAsset(obj); } return reference; } } /// /// Odin Inspector utility methods for working with addressables. /// public static class OdinAddressableUtility { private readonly static Action openAddressableWindowAction; static OdinAddressableUtility() { var type = TwoWaySerializationBinder.Default.BindToType("UnityEditor.AddressableAssets.GUI.AddressableAssetsWindow") ?? throw new Exception(""); var method = type.GetMethod("Init", Flags.AllMembers) ?? throw new Exception(""); openAddressableWindowAction = (Action)Delegate.CreateDelegate(typeof(Action), method); } public static IEnumerable EnumerateAllActualAndVirtualSubAssets(UnityEngine.Object mainAsset, string mainAssetPath) { if (mainAsset == null) { yield break; } Object[] subAssets = AssetDatabase.LoadAllAssetRepresentationsAtPath(mainAssetPath); foreach (Object subAsset in subAssets) { yield return subAsset; } // The sprites/textures in a sprite atlas are not sub assets of the atlas, but they are apparently // still part of the atlas in a way that the addressables system considers a sub asset. if (mainAsset is UnityEngine.U2D.SpriteAtlas atlas) { Object[] packables = atlas.GetPackables(); foreach (Object packable in packables) { if (packable == null) { continue; } if (!(packable is DefaultAsset packableFolder)) { yield return packable; continue; } string packablePath = AssetDatabase.GetAssetPath(packableFolder); if (!AssetDatabase.IsValidFolder(packablePath)) { continue; } string[] files = Directory.GetFiles(packablePath, "*.*", SearchOption.AllDirectories); foreach (string file in files) { if (file.EndsWith(".meta")) { continue; } Type assetType = AssetDatabase.GetMainAssetTypeAtPath(file); if (assetType != typeof(Sprite) && assetType != typeof(Texture2D)) { continue; } yield return AssetDatabase.LoadMainAssetAtPath(file); } } } } /// /// Opens the addressables group settings window. /// public static void OpenGroupsWindow() { openAddressableWindowAction(); } /// /// Opens the addressables labels settings window. /// public static void OpenLabelsWindow() { if (!AddressableAssetSettingsDefaultObject.SettingsExists) return; EditorWindow.GetWindow().Intialize(AddressableAssetSettingsDefaultObject.Settings); } /// /// Converts the specified object into an addressable. /// /// The object to make addressable. /// The addressable group to add the object to. public static void MakeAddressable(UnityEngine.Object obj, AddressableAssetGroup group) { if (!AddressableAssetSettingsDefaultObject.SettingsExists) return; var settings = AddressableAssetSettingsDefaultObject.Settings; var guid = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(obj)); var entry = settings.CreateOrMoveEntry(guid, group, false, false); entry.address = AssetDatabase.GUIDToAssetPath(guid); settings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryCreated, entry, false, true); } /// /// Gets the type targeted by an AssetReference type. For example, returns Texture for AssetReferenceTexture. /// Returns UnityEngine.Object for AssetReference type. /// /// A type of AssetReference, for example, AssetReferenceTexture. /// /// If the given type inherits AssetRefernceT<T>, then the method returns the generic T argument. /// If the given type is AssetReference, then the method returns UnityEngine.Object. /// /// Throws if given parameter is null. /// Throws if the given type does not inherit or is AssetReference. public static Type GetAssetReferenceTargetType(Type assetReferenceType) { if (assetReferenceType == null) throw new ArgumentNullException(nameof(assetReferenceType)); if (assetReferenceType.InheritsFrom(typeof(AssetReferenceT<>))) { var genericBase = assetReferenceType.GetGenericBaseType(typeof(AssetReferenceT<>)); return genericBase.GetGenericArguments()[0]; } else { return typeof(UnityEngine.Object); } } public static Type[] GetAssetReferenceValidMainAssetTypes(Type assetReferenceType) { if (assetReferenceType == null) throw new ArgumentNullException(nameof(assetReferenceType)); if (assetReferenceType.InheritsFrom(typeof(AssetReferenceSprite))) { return new Type[] { typeof(Sprite), typeof(SpriteAtlas), typeof(Texture2D) }; } else if (assetReferenceType.InheritsFrom(typeof(AssetReferenceAtlasedSprite))) { return new Type[] { typeof(SpriteAtlas) }; } return new Type[] { GetAssetReferenceTargetType(assetReferenceType) }; } /// /// Validate an asset against a list of AssetReferenceUIRestrictions. /// /// The restrictions to apply. /// The asset to validate. /// Returns true if the asset passes all restrictions. Otherwise false. /// Throws if Addressable Settings have not been created. /// Throws if restrictions or asset is null. public static bool ValidateAssetReferenceRestrictions(List restrictions, UnityEngine.Object asset) { return ValidateAssetReferenceRestrictions(restrictions, asset, out _); } /// /// Validate an asset against a list of AssetReferenceUIRestrictions. /// /// The restrictions to apply. /// The asset to validate. /// The first failed restriction. null if no restrictions failed. /// Returns true if the asset passes all restrictions. Otherwise false. /// Throws if Addressable Settings have not been created. /// Throws if restrictions or asset is null. public static bool ValidateAssetReferenceRestrictions(List restrictions, UnityEngine.Object asset, out AssetReferenceUIRestriction failedRestriction) { if (AddressableAssetSettingsDefaultObject.SettingsExists == false) throw new Exception("Addressable Settings have not been created."); _ = restrictions ?? throw new ArgumentNullException(nameof(restrictions)); _ = asset ?? throw new ArgumentNullException(nameof(asset)); for (int i = 0; i < restrictions.Count; i++) { if (restrictions[i] is AssetReferenceUILabelRestriction labels) { /* Unity, in all its wisdom, have apparently decided not to implement their AssetReferenceRestriction attributes in some versions(?) * So, to compensate, we're going to manually validate the label restriction attribute, so atleast that works. */ var guid = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(asset)); var entry = AddressableAssetSettingsDefaultObject.Settings.FindAssetEntry(guid, true); if (entry.labels.Any(x => labels.m_AllowedLabels.Contains(x)) == false) { failedRestriction = labels; return false; } } else if (restrictions[i].ValidateAsset(asset) == false) { failedRestriction = restrictions[i]; return false; } } failedRestriction = null; return true; } internal static TAssetReference CreateAssetReferenceGuid(string guid) where TAssetReference : AssetReference { return (TAssetReference) Activator.CreateInstance(typeof(TAssetReference), guid); } internal static TAssetReference CreateAssetReference(UnityEngine.Object obj) where TAssetReference : AssetReference { if (obj == null) { return CreateAssetReferenceGuid(null); } string guid = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(obj)); return CreateAssetReferenceGuid(guid); } internal static AddressableAssetEntry CreateFakeAddressableAssetEntry(string guid) { var entry = (AddressableAssetEntry) FormatterServices.GetUninitializedObject(typeof(AddressableAssetEntry)); OdinAddressableReflection.AddressableAssetEntry_mGUID_Field.SetValue(entry, guid); return entry; } } } #endif