Files
plugin-library/Assets/Plugins/Sirenix/Odin Inspector/Modules/Unity.Addressables.data

2988 lines
116 KiB
Plaintext
Raw Normal View History

2025-03-31 10:10:35 +08:00
/MSirenix.OdinInspector.Editor.Modules.ModuleData, Sirenix.OdinInspector.Editor'IDUnity.AddressablesVersion/System.Version, mscorlib Files/<01>System.Collections.Generic.List`1[[Sirenix.OdinInspector.Editor.Modules.ModuleData+ModuleFile, Sirenix.OdinInspector.Editor]], mscorlib/XSirenix.OdinInspector.Editor.Modules.ModuleData+ModuleFile, Sirenix.OdinInspector.Editor'PathAddressablesInspectors.csData/System.Byte[], mscorlib<08>&//-----------------------------------------------------------------------
// <copyright file="AddressablesInspectors.cs" company="Sirenix ApS">
// Copyright (c) Sirenix ApS. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
#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;
/// <summary>
/// <para>DisallowAddressableSubAssetField is used on AssetReference properties, and disallows and prevents assigned sub-assets to the asset reference.</para>
/// </summary>
/// <example>
/// <code>
/// [DisallowAddressableSubAssetField]
/// public AssetReference Reference;
/// </code>
/// </example>
/// <seealso cref="RegisterAssetReferenceAttributeForwardToChildAttribute" />
/// <seealso cref="AssetReferenceUILabelRestriction" />
[Conditional("UNITY_EDITOR")]
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.ReturnValue | AttributeTargets.Parameter)]
public class DisallowAddressableSubAssetFieldAttribute : Attribute
{
}
/// <summary>
/// <para>Registers an attribute to be applied to an AssetRefenece property, to be forwarded and applied to the AssetReference's child instead.</para>
/// <para>This allows attributes designed for use on UnityEngine.Objects to be used on AssetReference properties as well.</para>
/// <para>By default, <c>InlineEditorAttribute</c> and <c>PreviewFieldAttribute</c> are registered for forwarding.</para>
/// </summary>
/// <example>
/// <code>
/// [assembly: Sirenix.OdinInspector.Modules.RegisterAssetReferenceAttributeForwardToChild(typeof(InlineEditorAttribute))]
/// [assembly: Sirenix.OdinInspector.Modules.RegisterAssetReferenceAttributeForwardToChild(typeof(PreviewFieldAttribute))]
/// </code>
/// </example>
/// <seealso cref="DisallowAddressableSubAssetFieldAttribute" />
[Conditional("UNITY_EDITOR")]
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public class RegisterAssetReferenceAttributeForwardToChildAttribute : Attribute // TODO: Should this be a global attribute?
{
/// <summary>
/// The type of the attribute to forward.
/// </summary>
public readonly Type AttributeType;
/// <summary>
/// Registers the specified attribute to be copied and applied to the AssetReference's UnityEngine.Object child instead.
/// </summary>
/// <param name="attributeType">The attribute type to forward.</param>
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;
/// <summary>
/// Draws an AssetReference property.
/// </summary>
/// <typeparam name="T">The concrete type of AssetReference to be drawn. For example, <c>AssetReferenceTexture</c>.</typeparam>
[DrawerPriority(0, 1, 0)]
public class AssetReferenceDrawer<T> : OdinValueDrawer<T>, 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<AssetReferenceUIRestriction> restrictions;
private bool isSpriteAtlas;
protected override bool CanDrawValueProperty(InspectorProperty property)
{
return property.GetAttribute<DrawWithUnityAttribute>() == 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<AssetReferenceUIRestriction>();
foreach (var attr in this.Property.Attributes)
{
if (attr is AssetReferenceUIRestriction r)
{
this.restrictions.Add(r);
}
}
this.disallowSubAssets_Backing = Property.GetAttribute<DisallowAddressableSubAssetFieldAttribute>() != 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<SpriteAtlas>(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("<none>");
}
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<Object> 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<UnityEngine.Object>[subAssets.Count + 1];
items[0] = new GenericSelectorItem<UnityEngine.Object>("<none>", null);
for (int i = 0; i < subAssets.Count; i++)
{
var item = new GenericSelectorItem<UnityEngine.Object>(subAssets[i].name, subAssets[i]);
items[i + 1] = item;
}
var selector = new GenericSelector<UnityEngine.Object>("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<UnityEngine.Object> 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<AssetReferenceAtlasedSprite>())
{
this.SetMainAsset(entry.MainAsset);
return;
}
if (typeof(T).InheritsFrom<AssetReferenceSprite>())
{
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<AssetReferenceSprite>())
{
if (AssetDatabase.GetMainAssetTypeAtPath(path) == typeof(SpriteAtlas))
{
if (!(obj is SpriteAtlas))
{
instance.SetEditorSubObject(obj);
}
}
}
else if (typeof(T).InheritsFrom<AssetReferenceAtlasedSprite>())
{
// 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<T>(null);
}
}
}
/// <summary>
/// Draws an AssetLabelReference field.
/// </summary>
[DrawerPriority(0, 1, 0)]
public class AssetLabelReferenceDrawer : OdinValueDrawer<AssetLabelReference>, IDefinesGenericMenuItems
{
protected override bool CanDrawValueProperty(InspectorProperty property)
{
return property.GetAttribute<DrawWithUnityAttribute>() == 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 = "<none>";
}
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<string> 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());
}
}
/// <summary>
/// Odin Selector for Addressables.
/// </summary>
public class AddressableSelector : OdinSelector<AddressableAssetEntry>
{
//private static EditorPrefBool flatten = new EditorPrefBool("AddressablesSelector.Flatten", false);
public event Action<SelectionChangedType, IEnumerable<AddressableAssetEntry>> SelectionChangedWithType;
private static EditorPrefEnum<SelectorListMode> listMode = new EditorPrefEnum<SelectorListMode>("AddressablesSelector.ListMode", SelectorListMode.Group);
private readonly string title;
private readonly Type[] filterTypes;
private readonly List<AssetReferenceUIRestriction> restrictions;
internal bool ShowNonAddressables;
public override string Title => this.title;
/// <summary>
/// Initializes a AddressableSelector.
/// </summary>
/// <param name="title">The title of the selector. Set to null for no title.</param>
/// <param name="filterType">The type of UnityEngine.Object to be selectable. For example, UnityEngine.Texture. For no restriction, pass in UnityEngine.Object.</param>
/// <param name="labelRestrictions">The Addressable labels to restrict the selector to. Set to null for no label restrictions.</param>
/// <exception cref="ArgumentNullException">Throws if the filter type is null.</exception>
public AddressableSelector(string title, Type filterType, List<AssetReferenceUIRestriction> restrictions, Type assetReferenceType)
: this(title, new Type[] { filterType }, restrictions, assetReferenceType)
{
}
/// <summary>
/// Initializes a AddressableSelector.
/// </summary>
/// <param name="title">The title of the selector. Set to null for no title.</param>
/// <param name="filterTypes">The types of UnityEngine.Object to be selectable. For example, UnityEngine.Texture. For no restriction, pass in an array containing UnityEngine.Object.</param>
/// <param name="labelRestrictions">The Addressable labels to restrict the selector to. Set to null for no label restrictions.</param>
/// <exception cref="ArgumentNullException">Throws if the filter type is null.</exception>
public AddressableSelector(string title, Type[] filterTypes, List<AssetReferenceUIRestriction> restrictions, Type assetReferenceType)
{
this.title = title;
this.filterTypes = filterTypes ?? throw new ArgumentNullException(nameof(filterTypes));
this.restrictions = restrictions;
if (assetReferenceType != null)
{
if (assetReferenceType.InheritsFrom<AssetReference>() == 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<AddressableAssetEntry> 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<HierarchyProperty> enumerator = AssetDatabase_Internals.EnumerateAllAssets(searchFilter, false, AssetDatabaseSearchArea.InAssetsOnly);
if (enumerator.MoveNext())
{
var addedGuids = new HashSet<string>();
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, "<none> (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, $"<none> (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<string>
{
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<string> labels = null;
if (AddressableAssetSettingsDefaultObject.SettingsExists)
{
var settings = AddressableAssetSettingsDefaultObject.Settings;
labels = settings.GetLabels();
}
if (labels == null) labels = Array.Empty<string>();
tree.MenuItems.Add(new OdinMenuItem(tree, "<none>", null));
for (int i = 0; i < labels.Count; i++)
{
tree.MenuItems.Add(new OdinMenuItem(tree, labels[i], labels[i]));
}
}
}
/// <summary>
/// Resolves children for AssetReference properties, and implements the <c>RegisterAssetReferenceAttributeForwardToChild</c> behaviour.
/// </summary>
/// <typeparam name="T">The concrete type of AssetReference to be drawn. For example, <c>AssetReferenceTexture</c>.</typeparam>
public class AssetReferencePropertyResolver<T> : OdinPropertyResolver<T>
where T : AssetReference
{
private static readonly Type[] attributesToForward;
static AssetReferencePropertyResolver()
{
attributesToForward = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(x => x.GetCustomAttributes<RegisterAssetReferenceAttributeForwardToChildAttribute>())
.Cast<RegisterAssetReferenceAttributeForwardToChildAttribute>()
.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<Attribute> attributes = new List<Attribute>
{
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<TTarget> : IValueGetterSetter<T, TTarget>
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);
}
}
}
/// <summary>
/// Processes attributes for AssetReference properties.
/// </summary>
/// <typeparam name="T">The concrete type of AssetReference to be drawn. For example, <c>AssetReferenceTexture</c>.</typeparam>
public class AssetReferenceAttributeProcessor<T> : OdinAttributeProcessor<T>
where T : AssetReference
{
public override void ProcessSelfAttributes(InspectorProperty property, List<Attribute> attributes)
{
attributes.Add(new DoNotDrawAsReferenceAttribute());
attributes.Add(new HideReferenceObjectPickerAttribute());
attributes.Add(new SuppressInvalidAttributeErrorAttribute()); // TODO: Remove this with proper attribute forwarding support.
}
}
/// <summary>
/// Processes attributes for AssetLabelReference properties.
/// </summary>
public class AssetLabelReferenceAttributeProcessor : OdinAttributeProcessor<AssetLabelReference>
{
public override void ProcessSelfAttributes(InspectorProperty property, List<Attribute> attributes)
{
attributes.Add(new DoNotDrawAsReferenceAttribute());
attributes.Add(new HideReferenceObjectPickerAttribute());
}
}
/// <summary>
/// Implements conversion behaviour for addressables.
/// </summary>
[InitializeOnLoad]
internal class AssetReferenceConverter : ConvertUtility.ICustomConverter
{
private readonly Type type_AssetEntryTreeViewItem;
private WeakValueGetter<AddressableAssetEntry> 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<AddressableAssetEntry>(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<UnityEngine.Object>())
{
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<UnityEngine.Object>())
{
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<UnityEngine.Object>(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;
}
}
/// <summary>
/// Odin Inspector utility methods for working with addressables.
/// </summary>
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<UnityEngine.Object> 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);
}
}
}
}
/// <summary>
/// Opens the addressables group settings window.
/// </summary>
public static void OpenGroupsWindow()
{
openAddressableWindowAction();
}
/// <summary>
/// Opens the addressables labels settings window.
/// </summary>
public static void OpenLabelsWindow()
{
if (!AddressableAssetSettingsDefaultObject.SettingsExists) return;
EditorWindow.GetWindow<LabelWindow>().Intialize(AddressableAssetSettingsDefaultObject.Settings);
}
/// <summary>
/// Converts the specified object into an addressable.
/// </summary>
/// <param name="obj">The object to make addressable.</param>
/// <param name="group">The addressable group to add the object to.</param>
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);
}
/// <summary>
/// Gets the type targeted by an AssetReference type. For example, returns Texture for AssetReferenceTexture.
/// Returns UnityEngine.Object for AssetReference type.
/// </summary>
/// <param name="assetReferenceType">A type of AssetReference, for example, AssetReferenceTexture.</param>
/// <returns>
/// If the given type inherits AssetRefernceT&lt;T&gt;, then the method returns the generic T argument.
/// If the given type is AssetReference, then the method returns UnityEngine.Object.
/// </returns>
/// <exception cref="ArgumentNullException">Throws if given parameter is null.</exception>
/// <exception cref="ArgumentException">Throws if the given type does not inherit or is AssetReference.</exception>
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) };
}
/// <summary>
/// Validate an asset against a list of AssetReferenceUIRestrictions.
/// </summary>
/// <param name="restrictions">The restrictions to apply.</param>
/// <param name="asset">The asset to validate.</param>
/// <returns>Returns true if the asset passes all restrictions. Otherwise false.</returns>
/// <exception cref="Exception">Throws if Addressable Settings have not been created.</exception>
/// <exception cref="ArgumentNullException">Throws if restrictions or asset is null.</exception>
public static bool ValidateAssetReferenceRestrictions(List<AssetReferenceUIRestriction> restrictions, UnityEngine.Object asset)
{
return ValidateAssetReferenceRestrictions(restrictions, asset, out _);
}
/// <summary>
/// Validate an asset against a list of AssetReferenceUIRestrictions.
/// </summary>
/// <param name="restrictions">The restrictions to apply.</param>
/// <param name="asset">The asset to validate.</param>
/// <param name="failedRestriction">The first failed restriction. <c>null</c> if no restrictions failed.</param>
/// <returns>Returns true if the asset passes all restrictions. Otherwise false.</returns>
/// <exception cref="Exception">Throws if Addressable Settings have not been created.</exception>
/// <exception cref="ArgumentNullException">Throws if restrictions or asset is null.</exception>
public static bool ValidateAssetReferenceRestrictions(List<AssetReferenceUIRestriction> 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<TAssetReference>(string guid) where TAssetReference : AssetReference
{
return (TAssetReference) Activator.CreateInstance(typeof(TAssetReference), guid);
}
internal static TAssetReference CreateAssetReference<TAssetReference>(UnityEngine.Object obj) where TAssetReference : AssetReference
{
if (obj == null)
{
return CreateAssetReferenceGuid<TAssetReference>(null);
}
string guid = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(obj));
return CreateAssetReferenceGuid<TAssetReference>(guid);
}
internal static AddressableAssetEntry CreateFakeAddressableAssetEntry(string guid)
{
var entry = (AddressableAssetEntry) FormatterServices.GetUninitializedObject(typeof(AddressableAssetEntry));
OdinAddressableReflection.AddressableAssetEntry_mGUID_Field.SetValue(entry, guid);
return entry;
}
}
}
#endif0'PathAddressablesInspectors.cs.metaData0<08>fileFormatVersion: 2
guid: ba1664ec1a0467641a742eaadae146d4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
0'Path Internal.metaData0UfileFormatVersion: 2
guid: 6384593779a7421881db64cdb25db4c5
timeCreated: 17189848620 'Path7Sirenix.OdinInspector.Modules.Unity.Addressables.asmdefData0
<08>{
"name": "Sirenix.OdinInspector.Modules.Unity.Addressables",
"references": [
"Unity.Addressables",
"Unity.Addressables.Editor",
"Sirenix.Serialization",
"Sirenix.OdinInspector.Attributes",
"Sirenix.OdinInspector.Editor",
"Sirenix.Utilities.Editor",
"Sirenix.Utilities",
"Sirenix.OdinValidator.Editor"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": false,
"precompiledReferences": [
"Sirenix.Serialization.dll",
"Sirenix.OdinInspector.Attributes.dll",
"Sirenix.OdinInspector.Editor.dll",
"Sirenix.Utilities.Editor.dll",
"Sirenix.Utilities.dll",
"Sirenix.OdinValidator.Editor.dll"
],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}0 'Path<Sirenix.OdinInspector.Modules.Unity.Addressables.asmdef.metaData0 <08>fileFormatVersion: 2
guid: 3b4d8e09c665bfa47849130d8695171e
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
0 'PathValidators.metaData0<08>fileFormatVersion: 2
guid: b568c1d508ce0b74eb0025b8501d1c1e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
0'Path%Internal/OdinAddressableReflection.csData0x//-----------------------------------------------------------------------
// <copyright file="OdinAddressableReflection.cs" company="Sirenix ApS">
// Copyright (c) Sirenix ApS. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
#if UNITY_EDITOR
#if !SIRENIX_INTERNAL
#pragma warning disable
#endif
using System.Reflection;
using UnityEditor.AddressableAssets.Settings;
namespace Sirenix.OdinInspector.Modules.Addressables.Editor.Internal
{
internal static class OdinAddressableReflection
{
public static FieldInfo AddressableAssetEntry_mGUID_Field;
static OdinAddressableReflection()
{
AddressableAssetEntry_mGUID_Field = typeof(AddressableAssetEntry).GetField("m_GUID", BindingFlags.Instance | BindingFlags.NonPublic);
}
internal static void EnsureConstructed() { }
}
}
#endif0'Path*Internal/OdinAddressableReflection.cs.metaData0UfileFormatVersion: 2
guid: c1a8e9ae60bf4edebd0ec6052eb9064e
timeCreated: 17189848830'Path.Internal/OdinAddressableReflectionValidator.csData0//-----------------------------------------------------------------------
// <copyright file="OdinAddressableReflectionValidator.cs" company="Sirenix ApS">
// Copyright (c) Sirenix ApS. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
#if UNITY_EDITOR
#if SIRENIX_INTERNAL
using System.Collections;
using System.Reflection;
using Sirenix.OdinInspector.Editor.Validation;
using Sirenix.OdinInspector.Modules.Addressables.Editor.Internal;
[assembly: RegisterValidator(typeof(OdinAddressableReflectionValidator))]
namespace Sirenix.OdinInspector.Modules.Addressables.Editor.Internal
{
public class OdinAddressableReflectionValidator : GlobalValidator
{
public override IEnumerable RunValidation(ValidationResult result)
{
OdinAddressableReflection.EnsureConstructed();
FieldInfo[] fields = typeof(OdinAddressableReflection).GetFields(BindingFlags.Static | BindingFlags.Public);
for (var i = 0; i < fields.Length; i++)
{
if (fields[i].IsLiteral)
{
continue;
}
if (fields[i].GetValue(null) != null)
{
continue;
}
result.AddError($"[Odin Addressable Module]: {fields[i].Name} was not found.");
}
return null;
}
}
}
#endif
#endif0'Path3Internal/OdinAddressableReflectionValidator.cs.metaData0UfileFormatVersion: 2
guid: 97bf306f5a4249b192921db9152934f6
timeCreated: 17189848730'Path*Validators/AssetLabelReferenceValidator.csData0p//-----------------------------------------------------------------------
// <copyright file="AssetLabelReferenceValidator.cs" company="Sirenix ApS">
// Copyright (c) Sirenix ApS. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
#if UNITY_EDITOR
#if !SIRENIX_INTERNAL
#pragma warning disable
#endif
using UnityEngine;
using UnityEditor.AddressableAssets;
using Sirenix.OdinInspector.Editor.Validation;
using UnityEngine.AddressableAssets;
using Sirenix.OdinInspector.Modules.Addressables.Editor;
#if ODIN_VALIDATOR_3_1
[assembly: RegisterValidationRule(typeof(AssetLabelReferenceValidator), Description =
"This validator ensures that AssetLabelReferences marked with the Required attribute display an error " +
"message if they are not set. It can also be configured to require that all AssetLabelReferences be set " +
"by default; the Optional attribute can then be used to exclude specific AssetLabelReferences from " +
"validation.")]
#else
[assembly: RegisterValidator(typeof(AssetLabelReferenceValidator))]
#endif
namespace Sirenix.OdinInspector.Modules.Addressables.Editor
{
/// <summary>
/// Validator for AssetLabelReference values.
/// </summary>
public class AssetLabelReferenceValidator : ValueValidator<AssetLabelReference>
{
[Tooltip("If enabled, the validator will display an error message if the AssetLabelReference is not set. " +
"If disabled, the validator will only display an error message if the AssetLabelReference is set, but the " +
"assigned label does not exist.")]
[ToggleLeft]
public bool RequiredByDefault;
private bool required;
private bool optional;
private string requiredMessage;
protected override void Initialize()
{
var requiredAttr = this.Property.GetAttribute<RequiredAttribute>();
this.requiredMessage = requiredAttr?.ErrorMessage ?? $"<b>{this.Property.NiceName}</b> is required.";
if (this.RequiredByDefault)
{
required = true;
optional = Property.GetAttribute<OptionalAttribute>() != null;
}
else
{
required = requiredAttr != null;
optional = false;
}
}
protected override void Validate(ValidationResult result)
{
// If the Addressables settings have not been created, nothing else is really valid.
if (AddressableAssetSettingsDefaultObject.SettingsExists == false)
{
result.AddError("Addressables Settings have not been created.")
.WithButton("Open Settings Window", () => OdinAddressableUtility.OpenGroupsWindow());
return;
}
var value = Value?.labelString;
if (string.IsNullOrEmpty(value))
{
if (optional == false && required) // Optional == false & required? Nice.
{
result.AddError(requiredMessage).EnableRichText();
}
}
else
{
var labels = AddressableAssetSettingsDefaultObject.Settings.GetLabels();
if (labels.Contains(value) == false)
{
result.AddError($"Label <i>{value}</i> has not been created as a label.")
.WithButton("Open Label Settings", () => OdinAddressableUtility.OpenLabelsWindow());
}
}
}
}
}
#endif0'Path/Validators/AssetLabelReferenceValidator.cs.metaData0<08>fileFormatVersion: 2
guid: fcaf7dc3b9a98a545b301a1ea175055b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
0'Path%Validators/AssetReferenceValidator.csData0<08>.//-----------------------------------------------------------------------
// <copyright file="AssetReferenceValidator.cs" company="Sirenix ApS">
// Copyright (c) Sirenix ApS. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
#if UNITY_EDITOR
#if !SIRENIX_INTERNAL
#pragma warning disable
#endif
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditor;
using UnityEditor.AddressableAssets;
using Sirenix.OdinInspector.Editor.Validation;
using Sirenix.Utilities;
using Sirenix.Utilities.Editor;
using UnityEditor.AddressableAssets.Settings;
using UnityEngine.AddressableAssets;
using Sirenix.OdinInspector.Modules.Addressables.Editor;
#if ODIN_VALIDATOR_3_1
[assembly: RegisterValidationRule(typeof(AssetReferenceValidator), Description =
"This validator provides robust integrity checks for your asset references within Unity. " +
"It validates whether an asset reference has been assigned, and if it's missing, raises an error. " +
"It further checks the existence of the main asset at the assigned path, ensuring it hasn't been " +
"inadvertently deleted or moved. The validator also verifies if the assigned asset is addressable " +
"and, if not, offers a fix to make it addressable. Moreover, it ensures the asset adheres to " +
"specific label restrictions set through the AssetReferenceUILabelRestriction attribute. " +
"Lastly, it performs checks on any sub-object linked to the asset, making sure it hasn't gone missing. " +
"This comprehensive validation system prevents hard-to-spot bugs and errors, " +
"fostering a more robust and efficient development workflow.")]
#else
[assembly: RegisterValidator(typeof(AssetReferenceValidator))]
#endif
namespace Sirenix.OdinInspector.Modules.Addressables.Editor
{
public class AssetReferenceValidator : ValueValidator<AssetReference>
{
[Tooltip("If true and the AssetReference is not marked with the Optional attribute, " +
"the validator will display an error message if the AssetReference is not set. " +
"If false, the validator will only display an error message if the AssetReference is set, " +
"but the assigned asset does not exist.")]
[ToggleLeft]
public bool RequiredByDefault;
private bool required;
private bool optional;
private string requiredMessage;
private List<AssetReferenceUIRestriction> restrictions;
protected override void Initialize()
{
var requiredAttr = this.Property.GetAttribute<RequiredAttribute>();
this.requiredMessage = requiredAttr?.ErrorMessage ?? $"<b>{this.Property.NiceName}</b> is required.";
if (this.RequiredByDefault)
{
this.required = true;
this.optional = this.Property.GetAttribute<OptionalAttribute>() != null;
}
else
{
this.required = requiredAttr != null;
this.optional = false;
}
this.restrictions = new List<AssetReferenceUIRestriction>();
foreach (var attr in this.Property.Attributes)
{
if (attr is AssetReferenceUIRestriction r)
{
this.restrictions.Add(r);
}
}
}
protected override void Validate(ValidationResult result)
{
// If the Addressables settings have not been created, nothing else is really valid.
if (AddressableAssetSettingsDefaultObject.SettingsExists == false)
{
result.AddError("Addressables Settings have not been created.")
.WithButton("Open Settings Window", () => OdinAddressableUtility.OpenGroupsWindow());
return;
}
var assetReference = this.Value;
var assetReferenceHasBeenAssigned = !string.IsNullOrEmpty(assetReference?.AssetGUID);
// No item has been assigned.
if (!assetReferenceHasBeenAssigned)
{
if (optional == false && required) // Optional == false & required? Nice.
{
result.AddError(this.requiredMessage).EnableRichText();
}
return;
}
var assetPath = AssetDatabase.GUIDToAssetPath(assetReference.AssetGUID);
var mainAsset = AssetDatabase.LoadMainAssetAtPath(assetPath);
// The item has been assigned, but is now missing.
if (mainAsset == null)
{
result.AddError($"The previously assigned main asset with path <b>'{assetPath}'</b> is missing. GUID <b>'{assetReference.AssetGUID}'</b>");
return;
}
var addressableAssetEntry = AddressableAssetSettingsDefaultObject.Settings.FindAssetEntry(assetReference.AssetGUID, true);
var isAddressable = addressableAssetEntry != null;
// Somehow an item sneaked through all of unity's validation measures and ended up not being addressable
// while still ending up in the asset reference object field.
if (!isAddressable)
{
result.AddError("Assigned item is not addressable.")
.WithFix<MakeAddressableFixArgs>("Make Addressable", args => OdinAddressableUtility.MakeAddressable(mainAsset, args.Group));
}
// Check the assigned item against any and all label restrictions.
else
{
if (OdinAddressableUtility.ValidateAssetReferenceRestrictions(restrictions, mainAsset, out var failedRestriction) == false)
{
if (failedRestriction is AssetReferenceUILabelRestriction labelRestriction)
{
result.AddError($"Asset reference is restricted to items with these specific labels <b>'{string.Join(", ", labelRestriction.m_AllowedLabels)}'</b>. The currently assigned item has none of them.")
.WithFix<AddLabelsFixArgs>("Add Labels", args => SetLabels(mainAsset, args.AssetLabels));
}
else
{
result.AddError("Restriction failed: " + failedRestriction.ToString());
}
}
}
// The assigned item had a sub object, but it's missing.
if (!string.IsNullOrEmpty(assetReference.SubObjectName))
{
var subObjects = OdinAddressableUtility.EnumerateAllActualAndVirtualSubAssets(mainAsset, assetPath);
var hasMissingSubObject = true;
foreach (var subObject in subObjects)
{
if (subObject.name == assetReference.SubObjectName)
{
hasMissingSubObject = false;
break;
}
}
if (hasMissingSubObject)
{
result.AddError($"The previously assigned sub asset with name <b>'{assetReference.SubObjectName}'</b> is missing.").EnableRichText();
}
}
if (assetReference.ValidateAsset(mainAsset) || assetReference.ValidateAsset(assetPath))
return;
if (assetReference is AssetReferenceSprite && assetReference.editorAsset is Sprite)
return;
result.AddError($"{assetReference.GetType().GetNiceFullName()}.ValidateAsset failed to validate assigned asset.");
}
private static void SetLabels(UnityEngine.Object obj, List<AssetLabel> assetLabels)
{
if (!AddressableAssetSettingsDefaultObject.SettingsExists) return;
var settings = AddressableAssetSettingsDefaultObject.Settings;
var guid = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(obj));
var entry = settings.FindAssetEntry(guid, false);
foreach (var assetLabel in assetLabels.Where(a => a.Toggled))
{
entry.SetLabel(assetLabel.Label, true, false, false);
}
settings.SetDirty(AddressableAssetSettings.ModificationEvent.LabelAdded, entry, false, true);
}
private class MakeAddressableFixArgs
{
[ValueDropdown(nameof(GetGroups))]
[OnInspectorInit(nameof(SelectDefault))]
public AddressableAssetGroup Group;
private void SelectDefault()
{
this.Group = AddressableAssetSettingsDefaultObject.SettingsExists
? AddressableAssetSettingsDefaultObject.Settings.DefaultGroup
: null;
}
private static IEnumerable<ValueDropdownItem> GetGroups()
{
return !AddressableAssetSettingsDefaultObject.SettingsExists
? Enumerable.Empty<ValueDropdownItem>()
: AddressableAssetSettingsDefaultObject.Settings.groups
.Where(group => !group.ReadOnly)
.Select(group => new ValueDropdownItem(group.Name, group));
}
[Button(SdfIconType.ListNested), PropertySpace(8f)]
private void OpenAddressablesGroups() => OdinAddressableUtility.OpenGroupsWindow();
}
private class AddLabelsFixArgs
{
[HideIf("@true")]
public List<AssetLabel> AssetLabels
{
get
{
if (!AddressableAssetSettingsDefaultObject.SettingsExists) return this.assetLabels;
var settings = AddressableAssetSettingsDefaultObject.Settings;
var labels = settings
.GetLabels()
.Select(l => new AssetLabel { Label = l, Toggled = false })
.ToList();
foreach (var assetLabel in this.assetLabels)
{
var label = labels.FirstOrDefault(l => l.Label == assetLabel.Label);
if (label != null)
{
label.Toggled = assetLabel.Toggled;
}
}
this.assetLabels = labels;
return this.assetLabels;
}
}
private List<AssetLabel> assetLabels = new List<AssetLabel>();
[OnInspectorGUI]
private void Draw()
{
var togglesRect = EditorGUILayout.GetControlRect(false, Mathf.CeilToInt(this.AssetLabels.Count / 2f) * 20f);
for (var i = 0; i < this.AssetLabels.Count; i++)
{
var assetLabel = this.AssetLabels[i];
var toggleRect = togglesRect.SplitGrid(togglesRect.width / 2f, 20, i);
assetLabel.Toggled = GUI.Toggle(toggleRect, assetLabel.Toggled, assetLabel.Label);
}
if (!AddressableAssetSettingsDefaultObject.SettingsExists) return;
GUILayout.Space(8f);
var buttonsRect = EditorGUILayout.GetControlRect(false, 20f);
if (SirenixEditorGUI.SDFIconButton(buttonsRect, "Open Addressables Labels", SdfIconType.TagsFill))
{
OdinAddressableUtility.OpenLabelsWindow();
}
}
}
private class AssetLabel
{
public bool Toggled;
public string Label;
}
}
}
#endif0'Path*Validators/AssetReferenceValidator.cs.metaData0<08>fileFormatVersion: 2
guid: b44b08a1f58a83149988fde5ac600fe4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
0'Path7Validators/CheckDuplicateBundleDependenciesValidator.csData0 <08>//-----------------------------------------------------------------------
// <copyright file="CheckDuplicateBundleDependenciesValidator.cs" company="Sirenix ApS">
// Copyright (c) Sirenix ApS. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
#if UNITY_EDITOR && ODIN_VALIDATOR_3_1
#if !SIRENIX_INTERNAL
#pragma warning disable
#endif
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditor;
using UnityEditor.AddressableAssets;
using Sirenix.OdinInspector.Editor.Validation;
using Sirenix.Utilities;
using Sirenix.Utilities.Editor;
using UnityEditor.AddressableAssets.Settings;
using System.Collections;
using System;
using Sirenix.OdinValidator.Editor;
using Sirenix.OdinInspector.Modules.Addressables.Editor;
[assembly: RegisterValidationRule(typeof(CheckDuplicateBundleDependenciesValidator),
Description = "This validator detects potential duplicate asset dependencies in an addressable group, without the need for a build. " +
"For instance, imagine two prefabs in separate groups, both referencing the same material. Each group would then include the material " +
"and all its associated dependencies. " +
"To address this, the material should be marked as Addressable, either with one of the prefabs or in a distinct group.\n\n" +
"<b>Fixes: </b>Executing the fix will make the dependency addressable and move it to the specified group.\n\n" +
"<b>Exceptions: </b>It's important to note that duplicate assets aren't inherently problematic. For example, if certain assets are " +
"never accessed by the same user group, such as region-specific assets, these duplications might be desired or at least inconsequential. " +
"As every project is unique, decisions concerning duplicate asset dependencies should be considered on a case-by-case basis.")]
namespace Sirenix.OdinInspector.Modules.Addressables.Editor
{
public class CheckDuplicateBundleDependenciesValidator : GlobalValidator
{
private static Dictionary<GUID, List<string>> dependencyGroupMap = new Dictionary<GUID, List<string>>();
[Tooltip("The severity of the validation result.")]
public ValidatorSeverity ValidatorSeverity = ValidatorSeverity.Warning;
[Tooltip("Assets to ignore when validating.")]
[LabelText("Ignored GUIDs"), CustomValueDrawer(nameof(DrawGUIDEntry))]
public List<string> IgnoredGUIDs = new List<string>();
public override IEnumerable RunValidation(ValidationResult result)
{
dependencyGroupMap.Clear();
var addressableAssetSettings = AddressableAssetSettingsDefaultObject.Settings;
if (addressableAssetSettings == null) yield break;
foreach (var addressableAssetGroup in addressableAssetSettings.groups)
{
if (addressableAssetGroup == null) continue;
foreach (var addressableAssetEntry in addressableAssetGroup.entries)
{
var dependencyAssetPaths = AssetDatabase.GetDependencies(addressableAssetEntry.AssetPath)
.Where(assetPath => !assetPath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) &&
!assetPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase));
foreach (var dependencyAssetPath in dependencyAssetPaths)
{
var dependencyGUID = new GUID(AssetDatabase.AssetPathToGUID(dependencyAssetPath));
if (this.IgnoredGUIDs.Contains(dependencyGUID.ToString())) continue;
var dependencyAddressableAssetEntry = addressableAssetSettings.FindAssetEntry(dependencyGUID.ToString());
var isAddressable = dependencyAddressableAssetEntry != null;
if (isAddressable) continue;
if (!dependencyGroupMap.ContainsKey(dependencyGUID))
{
dependencyGroupMap.Add(dependencyGUID, new List<string>());
}
if (!dependencyGroupMap[dependencyGUID].Contains(addressableAssetGroup.Name))
{
dependencyGroupMap[dependencyGUID].Add(addressableAssetGroup.Name);
}
}
}
}
foreach (var kvp in dependencyGroupMap)
{
var dependencyGUID = kvp.Key;
var groups = kvp.Value;
if (groups.Count > 1)
{
var assetPath = AssetDatabase.GUIDToAssetPath(dependencyGUID.ToString());
var message = $"{assetPath} is duplicated in these groups: {string.Join(", ", groups)}";
result.Add(this.ValidatorSeverity, message).WithFix<FixArgs>(args =>
{
if (args.FixChoice == FixChoice.Ignore)
{
var sourceType = args.IgnoreForEveryone ? ConfigSourceType.Project : ConfigSourceType.Local;
var data = RuleConfig.Instance.GetRuleData<CheckDuplicateBundleDependenciesValidator>(sourceType);
data.IgnoredGUIDs.Add(dependencyGUID.ToString());
RuleConfig.Instance.SetAndSaveRuleData(data, sourceType);
return;
}
var obj = AssetDatabase.LoadAssetAtPath(assetPath, typeof(UnityEngine.Object));
AddressableAssetGroup group;
if (args.Group == "Create New Group")
{
if (args.GroupName.IsNullOrWhitespace()) return;
group = addressableAssetSettings.FindGroup(args.GroupName);
if (group == null)
{
group = addressableAssetSettings.CreateGroup(args.GroupName, false, false, false, null);
}
}
else
{
group = addressableAssetSettings.FindGroup(args.Group);
if (group == null)
{
group = addressableAssetSettings.CreateGroup(args.Group, false, false, false, null);
}
}
OdinAddressableUtility.MakeAddressable(obj, group);
}, false).WithModifyRuleDataContextClick<CheckDuplicateBundleDependenciesValidator>("Ignore", data =>
{
data.IgnoredGUIDs.Add(dependencyGUID.ToString());
}).SetSelectionObject(AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(AssetDatabase.GUIDToAssetPath(dependencyGUID.ToString())));
}
}
}
private string DrawGUIDEntry(string guid)
{
var assetPath = AssetDatabase.GUIDToAssetPath(guid);
EditorGUILayout.TextArea(assetPath, SirenixGUIStyles.MultiLineLabel);
EditorGUILayout.TextField(guid);
return guid;
}
private enum FixChoice
{
AddToGroup,
Ignore,
}
private class FixArgs
{
[EnumToggleButtons, HideLabel]
public FixChoice FixChoice;
[PropertySpace(10)]
[ValueDropdown("Groups")]
//[Title("Group To Add To", TitleAlignment = TitleAlignments.Centered)]
[ShowIf(nameof(FixChoice), FixChoice.AddToGroup, Animate = false)]
public string Group = "Duplicate Asset Isolation";
[ValidateInput(nameof(ValidateGroupName), "The group name cannot be empty")]
[ShowIf(nameof(ShowNewGroupName), Animate = false)]
public string GroupName;
[LabelWidth(120f)]
[PropertySpace(10)]
[ShowIf("FixChoice", FixChoice.Ignore, Animate = false)]
public bool IgnoreForEveryone = true;
[OnInspectorGUI]
[PropertySpace(10)]
[DetailedInfoBox("Note that duplicate assets may not always be an issue", "Note that duplicate assets may not always be an issue. If assets will never be requested by the same set of users (for example, region-specific assets), then duplicate dependencies may be desired, or at least inconsequential. Each Project is unique, so fixing duplicate asset dependencies should be evaluated on a case by case basis")]
private void Dummy() { }
private bool ShowNewGroupName => this.FixChoice != FixChoice.Ignore && this.Group == "Create New Group";
private bool ValidateGroupName() => !this.GroupName.IsNullOrWhitespace();
private IEnumerable<string> Groups()
{
var addressableAssetSettings = AddressableAssetSettingsDefaultObject.Settings;
return addressableAssetSettings == null
? Enumerable.Empty<string>()
: addressableAssetSettings.groups
.Where(group => group != null && group.Name != "Built In Data")
.Select(group => group.Name)
.Append("Duplicate Asset Isolation")
.Prepend("Create New Group");
}
}
}
}
#endif0!'Path<Validators/CheckDuplicateBundleDependenciesValidator.cs.metaData0"<08>fileFormatVersion: 2
guid: bdc8ee2cf75a17644a0bd81a965cc2e0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
0#'PathGValidators/CheckResourcesToAddressableDuplicateDependenciesValidator.csData0$<08>//-----------------------------------------------------------------------
// <copyright file="CheckResourcesToAddressableDuplicateDependenciesValidator.cs" company="Sirenix ApS">
// Copyright (c) Sirenix ApS. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
#if UNITY_EDITOR && ODIN_VALIDATOR_3_1
#if !SIRENIX_INTERNAL
#pragma warning disable
#endif
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditor;
using UnityEditor.AddressableAssets;
using Sirenix.OdinInspector.Editor.Validation;
using Sirenix.Utilities.Editor;
using System.Collections;
using System;
using System.IO;
using Sirenix.OdinValidator.Editor;
using Sirenix.OdinInspector.Modules.Addressables.Editor;
[assembly: RegisterValidationRule(typeof(CheckResourcesToAddressableDuplicateDependenciesValidator),
Description = "This validator identifies dependencies that are duplicated in both addressable groups and the \"Resources\" folder.\n\n" +
"These duplications mean that data will be included in both the application build and the addressables build.\n\n" +
"You can decide to simply ignore these duplicated dependencies if this behavior is desired, or use the provided fix " +
"to move the asset outside of the \"Resources\" folder.")]
namespace Sirenix.OdinInspector.Modules.Addressables.Editor
{
public class CheckResourcesToAddressableDuplicateDependenciesValidator : GlobalValidator
{
[Tooltip("The severity of the validation result.")]
public ValidatorSeverity ValidatorSeverity = ValidatorSeverity.Warning;
[Tooltip("Assets to ignore when validating.")]
[LabelText("Ignored GUIDs"), CustomValueDrawer(nameof(DrawGUIDEntry))]
public List<string> IgnoredGUIDs = new List<string>();
public override IEnumerable RunValidation(ValidationResult result)
{
var addressableAssetSettings = AddressableAssetSettingsDefaultObject.Settings;
if (addressableAssetSettings == null) yield break;
foreach (var addressableAssetGroup in addressableAssetSettings.groups)
{
if (addressableAssetGroup == null) continue;
foreach (var addressableAssetEntry in addressableAssetGroup.entries)
{
var dependencyAssetPaths = AssetDatabase.GetDependencies(addressableAssetEntry.AssetPath)
.Where(assetPath => !assetPath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) &&
!assetPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase));
foreach (var dependencyAssetPath in dependencyAssetPaths)
{
var dependencyGUID = new GUID(AssetDatabase.AssetPathToGUID(dependencyAssetPath));
if (this.IgnoredGUIDs.Contains(dependencyGUID.ToString())) continue;
var dependencyAddressableAssetEntry = addressableAssetSettings.FindAssetEntry(dependencyGUID.ToString());
var isAddressable = dependencyAddressableAssetEntry != null;
if (isAddressable) continue;
if (!IsInsideResourcesFolder(dependencyAssetPath)) continue;
result.Add(this.ValidatorSeverity, $"{dependencyAssetPath} is duplicated in addressable data and resource folders.")
.WithFix<FixArgs>(args =>
{
if (args.FixChoice == FixChoice.Ignore)
{
var sourceType = args.IgnoreForEveryone ? ConfigSourceType.Project : ConfigSourceType.Local;
var data = RuleConfig.Instance.GetRuleData<CheckResourcesToAddressableDuplicateDependenciesValidator>(sourceType);
data.IgnoredGUIDs.Add(dependencyGUID.ToString());
RuleConfig.Instance.SetAndSaveRuleData(data, sourceType);
return;
}
if (!ValidNewFolder(args.NewFolder, out _)) return;
if (!AssetDatabase.IsValidFolder(args.NewFolder))
{
Directory.CreateDirectory(new DirectoryInfo(args.NewFolder).FullName);
AssetDatabase.Refresh();
}
var newPath = $"{args.NewFolder}/{Path.GetFileName(dependencyAssetPath)}";
AssetDatabase.MoveAsset(dependencyAssetPath, newPath);
}, false).WithModifyRuleDataContextClick<CheckResourcesToAddressableDuplicateDependenciesValidator>("Ignore", data =>
{
data.IgnoredGUIDs.Add(dependencyGUID.ToString());
}).SetSelectionObject(AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(AssetDatabase.GUIDToAssetPath(dependencyGUID.ToString())));
yield break;
}
}
}
}
private string DrawGUIDEntry(string guid)
{
var assetPath = AssetDatabase.GUIDToAssetPath(guid);
EditorGUILayout.TextArea(assetPath, SirenixGUIStyles.MultiLineLabel);
EditorGUILayout.TextField(guid);
return guid;
}
private static bool IsInsideResourcesFolder(string path)
{
var pathElements = path.Split('/');
foreach (var pathElement in pathElements)
{
if (pathElement.Equals("Resources", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private static bool ValidNewFolder(string path, out string message)
{
if (IsInsideResourcesFolder(path))
{
message = "The asset cannot be moved into a 'Resources' folder";
return false;
}
if (!path.StartsWith("Assets/"))
{
message = "The asset must be inside the 'Assets' folder";
return false;
}
message = "The folder is valid";
return true;
}
private enum FixChoice
{
MoveAsset,
Ignore,
}
private class FixArgs
{
[HideLabel]
[EnumToggleButtons]
public FixChoice FixChoice;
[FolderPath]
[PropertySpace(10)]
[ValidateInput(nameof(ValidateFolderPath))]
[ShowIf("FixChoice", FixChoice.MoveAsset, Animate = false)]
public string NewFolder = "Assets/Resources_moved";
[LabelWidth(120f)]
[PropertySpace(10)]
[ShowIf("FixChoice", FixChoice.Ignore, Animate = false)]
public bool IgnoreForEveryone = true;
private bool ValidateFolderPath(string path, ref string message)
{
return ValidNewFolder(path, out message);
}
}
}
}
#endif0%'PathLValidators/CheckResourcesToAddressableDuplicateDependenciesValidator.cs.metaData0&<08>fileFormatVersion: 2
guid: f75aebe03a9aa4a4b82d2b54dcc34de5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
0''Path@Validators/DisallowAddressableSubAssetFieldAttributeValidator.csData0(_//-----------------------------------------------------------------------
// <copyright file="DisallowAddressableSubAssetFieldAttributeValidator.cs" company="Sirenix ApS">
// Copyright (c) Sirenix ApS. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
#if UNITY_EDITOR
#if !SIRENIX_INTERNAL
#pragma warning disable
#endif
using Sirenix.OdinInspector.Editor.Validation;
using Sirenix.OdinInspector.Modules.Addressables.Editor;
using UnityEngine.AddressableAssets;
[assembly: RegisterValidator(typeof(DisallowAddressableSubAssetFieldAttributeValidator))]
namespace Sirenix.OdinInspector.Modules.Addressables.Editor
{
/// <summary>
/// Validator for the DisallowAddressableSubAssetFieldAttribute.
/// </summary>
public class DisallowAddressableSubAssetFieldAttributeValidator : AttributeValidator<DisallowAddressableSubAssetFieldAttribute, AssetReference>
{
protected override void Validate(ValidationResult result)
{
if (this.Value != null && string.IsNullOrEmpty(this.Value.SubObjectName) == false)
{
result.AddError("Sub-asset references is not allowed on this field.")
.WithFix("Remove Sub-Asset", () => this.Value.SubObjectName = null, true);
}
}
}
}
#endif0)'PathEValidators/DisallowAddressableSubAssetFieldAttributeValidator.cs.metaData0*<08>fileFormatVersion: 2
guid: 5775d33ffc1143149c4f425f693b04b6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
0+'Path7Validators/MissingAddressableGroupReferenceValidator.csData0,D//-----------------------------------------------------------------------
// <copyright file="MissingAddressableGroupReferenceValidator.cs" company="Sirenix ApS">
// Copyright (c) Sirenix ApS. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
#if UNITY_EDITOR && ODIN_VALIDATOR_3_1
#if !SIRENIX_INTERNAL
#pragma warning disable
#endif
using System.Collections.Generic;
using UnityEditor.AddressableAssets;
using Sirenix.OdinInspector.Editor.Validation;
using UnityEditor.AddressableAssets.Settings;
using System.Collections;
using Sirenix.OdinInspector.Modules.Addressables.Editor;
[assembly: RegisterValidator(typeof(MissingAddressableGroupReferenceValidator))]
namespace Sirenix.OdinInspector.Modules.Addressables.Editor
{
public class MissingAddressableGroupReferenceValidator : GlobalValidator
{
public override IEnumerable RunValidation(ValidationResult result)
{
var addressableAssetSettings = AddressableAssetSettingsDefaultObject.Settings;
if (addressableAssetSettings == null) yield break;
var missingGroupIndices = new List<int>();
for (var i = 0; i < addressableAssetSettings.groups.Count; i++)
{
var group = addressableAssetSettings.groups[i];
if (group == null)
{
missingGroupIndices.Add(i);
}
}
if (missingGroupIndices.Count > 0)
{
result.Add(ValidatorSeverity.Error, "Addressable groups contains missing references").WithFix("Delete missing reference", () =>
{
for (var i = missingGroupIndices.Count - 1; i >= 0; i--)
{
addressableAssetSettings.groups.RemoveAt(missingGroupIndices[i]);
addressableAssetSettings.SetDirty(AddressableAssetSettings.ModificationEvent.GroupRemoved, null, true, true);
}
});
}
}
}
}
#endif0-'Path<Validators/MissingAddressableGroupReferenceValidator.cs.metaData0.<08>fileFormatVersion: 2
guid: 4ae55abdaf19ef4498d1a2fbe1bd9f9b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: