1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Addressablesの内部データをソート

Posted at

2022/12/17 : 初稿
Unity : 2021.3.15f1
Addressables : 1.20.0

やりたいこと

Addressablesの.assetファイルですが、
グループやエントリーが登録順に保存されるので
チーム開発してるとgitで差分がわかりにくいしコンフリクトしやすい。

そこで、常にグループ名&エントリー名でソートした状態で保存されるようにしたい。

手順

残念ながらパッケージそのものをカスタマイズせざるを得ない模様。

  1. Addressablesのパッケージをカスタムパッケージ化
  2. グループやエントリーの追加削除時にソートするようにコードを追記

Addressablesのパッケージをカスタムパッケージ化

2022/12/17現在、パッケージの推奨バージョンは1.19.19なのですが、
今回はフライングして1.20.0を使っています。

  1. インポートしてないならPackage ManagerからAddressables1.20.0をインポート
  2. [プロジェクトフォルダ]/Library/PackageCache/com.unity.addressables@1.20.0[プロジェクトフォルダ]/Packagesにコピー

グループやエントリーの追加削除時にソートするようにコードを追記

変更するファイルは二つです。追記部分はyukimotoで検索。
やり方に不備はあるかもしれませんが、とりあえずこれで動いているのでヨシとします。

AddressableAssetGroup.cs

グループ内エントリーに変更があった時、アドレス順にソートするようにします。

AddressableAssetGroup.cs
./Packages/com.unity.addressables@1.20.0/Editor/Settings/AddressableAssetGroup.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Formatters.Binary;
using UnityEditor.AddressableAssets.Settings.GroupSchemas;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.Serialization;

namespace UnityEditor.AddressableAssets.Settings
{
    /// <summary>
    /// Contains the collection of asset entries associated with this group.
    /// </summary>
    [Serializable]
    public class AddressableAssetGroup : ScriptableObject, IComparer<AddressableAssetEntry>, ISerializationCallbackReceiver
    {
        internal static GUIContent RemoveSchemaContent = new GUIContent("Remove Schema", "Remove this schema.");
        internal static GUIContent MoveSchemaUpContent = new GUIContent("Move Up", "Move schema up one in list.");
        internal static GUIContent MoveSchemaDownContent = new GUIContent("Move Down", "Move schema down one in list.");
        internal static GUIContent ExpandSchemaContent = new GUIContent("Expand All", "Expand all settings within schema.");


        [FormerlySerializedAs("m_name")]
        [SerializeField]
        string m_GroupName;
        [FormerlySerializedAs("m_data")]
        [SerializeField]
        KeyDataStore m_Data;
        [FormerlySerializedAs("m_guid")]
        [SerializeField]
        string m_GUID;
        [FormerlySerializedAs("m_serializeEntries")]
        [SerializeField]
        List<AddressableAssetEntry> m_SerializeEntries = new List<AddressableAssetEntry>();
        [FormerlySerializedAs("m_readOnly")]
        [SerializeField]
        internal bool m_ReadOnly;
        [FormerlySerializedAs("m_settings")]
        [SerializeField]
        AddressableAssetSettings m_Settings;
        [FormerlySerializedAs("m_schemaSet")]
        [SerializeField]
        AddressableAssetGroupSchemaSet m_SchemaSet = new AddressableAssetGroupSchemaSet();

        Dictionary<string, AddressableAssetEntry> m_EntryMap = new Dictionary<string, AddressableAssetEntry>();
        List<AddressableAssetEntry> m_FolderEntryCache = null;
        List<AddressableAssetEntry> m_AssetCollectionEntryCache = null;


+        // yukimoto added.
+        struct EntryMapSortItem
+        {
+            public string Key;
+            public AddressableAssetEntry Entry;
+        }
+        public void SortAddress()
+        {
+            if (m_SerializeEntries != null)
+            {
+                m_SerializeEntries = m_SerializeEntries.OrderBy(entry => entry.address).ToList();
+            }
+            if (m_EntryMap != null)
+           {
+                List<EntryMapSortItem> items = new List<EntryMapSortItem>();
+                foreach (var pair in m_EntryMap)
+               {
+                   EntryMapSortItem item = default;
+                   item.Key = pair.Key;
+                    item.Entry = pair.Value;
+                    items.Add(item);
+                }
+                items.Sort((a, b) => string.Compare(a.Entry.address, b.Entry.address));
+                m_EntryMap.Clear();
+                foreach (var item in items)
+                {
+                    m_EntryMap.Add(item.Key, item.Entry);
+                }
+            }
+        }

        /// <summary>
        /// If true, this Group is likely marked 'Cannot Change Post Release', but has a modified asset since the previous build.
        /// </summary>
        public bool FlaggedDuringContentUpdateRestriction
        {
            get
            {
                foreach (var e in entries)
                    if (e.FlaggedDuringContentUpdateRestriction)
                        return true;

                return false;
            }
        }

        internal void RefreshEntriesCache()
        {
            m_FolderEntryCache = new List<AddressableAssetEntry>();
            m_AssetCollectionEntryCache = new List<AddressableAssetEntry>();
            foreach (AddressableAssetEntry e in entries)
            {
                if (!string.IsNullOrEmpty(e.AssetPath) && e.MainAssetType == typeof(DefaultAsset) && AssetDatabase.IsValidFolder(e.AssetPath))
                    m_FolderEntryCache.Add(e);
#pragma warning disable 0618
                else if (!string.IsNullOrEmpty(e.AssetPath) && e.AssetPath.EndsWith(".asset") && e.MainAssetType == typeof(AddressableAssetEntryCollection))
                    m_AssetCollectionEntryCache.Add(e);
#pragma warning restore 0618
            }
        }

        /// <summary>
        /// The group name.
        /// </summary>
        public virtual string Name
        {
            get
            {
                if (string.IsNullOrEmpty(m_GroupName))
                    m_GroupName = Guid;

                return m_GroupName;
            }
            set
            {
                string newName = value;
                newName = newName.Replace('/', '-');
                newName = newName.Replace('\\', '-');
                if (newName != value)
                    Debug.Log("Group names cannot include '\\' or '/'.  Replacing with '-'. " + m_GroupName);
                if (m_GroupName != newName)
                {
                    string previousName = m_GroupName;

                    string guid;
                    long localId;
                    if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(this, out guid, out localId))
                    {
                        var path = AssetDatabase.GUIDToAssetPath(guid);
                        if (!string.IsNullOrEmpty(path))
                        {
                            var folder = Path.GetDirectoryName(path);
                            var extension = Path.GetExtension(path);
                            var newPath = $"{folder}/{newName}{extension}".Replace('\\', '/');
                            if (path != newPath)
                            {
                                var setPath = AssetDatabase.MoveAsset(path, newPath);
                                bool success = false;
                                if (string.IsNullOrEmpty(setPath))
                                {
                                    name = m_GroupName = newName;
                                    success = RenameSchemaAssets();
                                }

                                if (success == false)
                                {
                                    //unable to rename group due to invalid file name
                                    Debug.LogError("Rename of Group failed. " + setPath);
                                    name = m_GroupName = previousName;
                                }
                            }
                        }
                    }
                    else
                    {
                        //this isn't a valid asset, which means it wasn't persisted, so just set the object name to the desired display name.
                        name = m_GroupName = newName;
                    }

                    SetDirty(AddressableAssetSettings.ModificationEvent.GroupRenamed, this, true, true);
                }
                else if (name != newName)
                {
                    name = m_GroupName;
                    SetDirty(AddressableAssetSettings.ModificationEvent.GroupRenamed, this, true, true);
                }
            }
        }
        /// <summary>
        /// The group GUID.
        /// </summary>
        public virtual string Guid
        {
            get
            {
                if (string.IsNullOrEmpty(m_GUID))
                    m_GUID = GUID.Generate().ToString();
                return m_GUID;
            }
        }

        /// <summary>
        /// List of schemas for this group.
        /// </summary>
        public List<AddressableAssetGroupSchema> Schemas { get { return m_SchemaSet.Schemas; } }

        /// <summary>
        /// Get the types of added schema for this group.
        /// </summary>
        public List<Type> SchemaTypes { get { return m_SchemaSet.Types; } }

        string GetSchemaAssetPath(Type type)
        {
            return Settings.IsPersisted ? (Settings.GroupSchemaFolder + "/" + Name + "_" + type.Name + ".asset") : string.Empty;
        }

        /// <summary>
        /// Adds a copy of the provided schema object.
        /// </summary>
        /// <param name="schema">The schema to add. A copy will be made and saved in a folder relative to the main Addressables settings asset. </param>
        /// <param name="postEvent">Determines if this method call will post an event to the internal addressables event system</param>
        /// <returns>The created schema object.</returns>
        public AddressableAssetGroupSchema AddSchema(AddressableAssetGroupSchema schema, bool postEvent = true)
        {
            var added = m_SchemaSet.AddSchema(schema, GetSchemaAssetPath);
            if (added != null)
            {
                added.Group = this;
                if (m_Settings && m_Settings.IsPersisted)
                    EditorUtility.SetDirty(added);

                SetDirty(AddressableAssetSettings.ModificationEvent.GroupSchemaAdded, this, postEvent, true);

                AssetDatabase.SaveAssets();
            }
            return added;
        }

        /// <summary>
        /// Creates and adds a schema of a given type to this group.  The schema asset will be created in the GroupSchemas directory relative to the settings asset.
        /// </summary>
        /// <param name="type">The schema type. This type must not already be added.</param>
        /// <param name="postEvent">Determines if this method call will post an event to the internal addressables event system</param>
        /// <returns>The created schema object.</returns>
        public AddressableAssetGroupSchema AddSchema(Type type, bool postEvent = true)
        {
            var added = m_SchemaSet.AddSchema(type, GetSchemaAssetPath);
            if (added != null)
            {
                added.Group = this;
                if (m_Settings && m_Settings.IsPersisted)
                    EditorUtility.SetDirty(added);

                SetDirty(AddressableAssetSettings.ModificationEvent.GroupSchemaAdded, this, postEvent, true);

                AssetDatabase.SaveAssets();
            }
            return added;
        }

        /// <summary>
        /// Creates and adds a schema of a given type to this group.
        /// </summary>
        /// <param name="postEvent">Determines if this method call will post an event to the internal addressables event system</param>
        /// <typeparam name="TSchema">The schema type. This type must not already be added.</typeparam>
        /// <returns>The created schema object.</returns>
        public TSchema AddSchema<TSchema>(bool postEvent = true) where TSchema : AddressableAssetGroupSchema
        {
            return AddSchema(typeof(TSchema), postEvent) as TSchema;
        }

        /// <summary>
        ///  Remove a given schema from this group.
        /// </summary>
        /// <param name="type">The schema type.</param>
        /// <param name="postEvent">Determines if this method call will post an event to the internal addressables event system</param>
        /// <returns>True if the schema was found and removed, false otherwise.</returns>
        public bool RemoveSchema(Type type, bool postEvent = true)
        {
            if (!m_SchemaSet.RemoveSchema(type))
                return false;

            SetDirty(AddressableAssetSettings.ModificationEvent.GroupSchemaRemoved, this, postEvent, true);
            return true;
        }

        /// <summary>
        ///  Remove a given schema from this group.
        /// </summary>
        /// <param name="postEvent">Determines if this method call will post an event to the internal addressables event system</param>
        /// <typeparam name="TSchema">The schema type.</typeparam>
        /// <returns>True if the schema was found and removed, false otherwise.</returns>
        public bool RemoveSchema<TSchema>(bool postEvent = true)
        {
            return RemoveSchema(typeof(TSchema), postEvent);
        }

        /// <summary>
        /// Gets an added schema of the specified type.
        /// </summary>
        /// <typeparam name="TSchema">The schema type.</typeparam>
        /// <returns>The schema if found, otherwise null.</returns>
        public TSchema GetSchema<TSchema>() where TSchema : AddressableAssetGroupSchema
        {
            return GetSchema(typeof(TSchema)) as TSchema;
        }

        /// <summary>
        /// Gets an added schema of the specified type.
        /// </summary>
        /// <param name="type">The schema type.</param>
        /// <returns>The schema if found, otherwise null.</returns>
        public AddressableAssetGroupSchema GetSchema(Type type)
        {
            return m_SchemaSet.GetSchema(type);
        }

        /// <summary>
        /// Checks if the group contains a schema of a given type.
        /// </summary>
        /// <typeparam name="TSchema">The schema type.</typeparam>
        /// <returns>True if the schema type or subclass has been added to this group.</returns>
        public bool HasSchema<TSchema>()
        {
            return HasSchema(typeof(TSchema));
        }

        /// <summary>
        /// Removes all schemas and optionally deletes the assets associated with them.
        /// </summary>
        /// <param name="deleteAssets">If true, the schema assets will also be deleted.</param>
        /// <param name="postEvent">Determines if this method call will post an event to the internal addressables event system</param>
        public void ClearSchemas(bool deleteAssets, bool postEvent = true)
        {
            m_SchemaSet.ClearSchemas(deleteAssets);
            SetDirty(AddressableAssetSettings.ModificationEvent.GroupRemoved, this, postEvent, true);
        }

        /// <summary>
        /// Checks if the group contains a schema of a given type.
        /// </summary>
        /// <param name="type">The schema type.</param>
        /// <returns>True if the schema type or subclass has been added to this group.</returns>
        public bool HasSchema(Type type)
        {
            return GetSchema(type) != null;
        }

        /// <summary>
        /// Is this group read only.  This is normally false.  Built in resources (resource folders and the scene list) are put into a special read only group.
        /// </summary>
        public virtual bool ReadOnly
        {
            get { return m_ReadOnly; }
        }

        /// <summary>
        /// The AddressableAssetSettings that this group belongs to.
        /// </summary>
        public AddressableAssetSettings Settings
        {
            get
            {
                if (m_Settings == null)
                    m_Settings = AddressableAssetSettingsDefaultObject.Settings;

                return m_Settings;
            }
        }

        /// <summary>
        /// The collection of asset entries.
        /// </summary>
        public virtual ICollection<AddressableAssetEntry> entries
        {
            get
            {
                return m_EntryMap.Values;
            }
        }

        internal Dictionary<string, AddressableAssetEntry> EntryMap => m_EntryMap;

        internal ICollection<AddressableAssetEntry> FolderEntries
        {
            get
            {
                if (m_FolderEntryCache == null)
                    RefreshEntriesCache();
                return m_FolderEntryCache;
            }
        }
        
        internal ICollection<AddressableAssetEntry> AssetCollectionEntries
        {
            get
            {
                if (m_AssetCollectionEntryCache == null)
                    RefreshEntriesCache();
                return m_AssetCollectionEntryCache;
            }
        }

        /// <summary>
        /// Is the default group.
        /// </summary>
        public virtual bool Default
        {
            get { return Guid == Settings.DefaultGroup.Guid; }
        }

        /// <summary>
        /// Compares two asset entries based on their guids.
        /// </summary>
        /// <param name="x">The first entry to compare.</param>
        /// <param name="y">The second entry to compare.</param>
        /// <returns>Returns 0 if both entries are null or equivalent.
        /// Returns -1 if the first entry is null or the first entry precedes the second entry in the sort order.
        /// Returns 1 if the second entry is null or the first entry follows the second entry in the sort order.</returns>
        public virtual int Compare(AddressableAssetEntry x, AddressableAssetEntry y)
        {
            if (x == null && y == null)
                return 0;
            if (x == null)
                return -1;
            if (y == null)
                return 1;
            return x.guid.CompareTo(y.guid);
        }

        internal void SerializeForHash(BinaryFormatter formatter, Stream stream)
        {
            formatter.Serialize(stream, m_GroupName);
            formatter.Serialize(stream, m_GUID);
            formatter.Serialize(stream, entries.Count);
            foreach (var e in entries)
                e.SerializeForHash(formatter, stream);
            formatter.Serialize(stream, m_ReadOnly);
            //TODO: serialize group data
        }

        /// <summary>
        /// Converts data to serializable format.
        /// </summary>
        public void OnBeforeSerialize()
        {
            if (m_SerializeEntries == null)
            {
                m_SerializeEntries = new List<AddressableAssetEntry>(entries.Count);
                foreach (var e in entries)
                    m_SerializeEntries.Add(e);
            }


+            // yukimoto added.
+			SortAddress();
        }

        /// <summary>
        /// Converts data from serializable format.
        /// </summary>
        public void OnAfterDeserialize()
        {
            ResetEntryMap();
        }

        internal void ResetEntryMap()
        {


+            // yukimoto added.
+            SortAddress();

            m_EntryMap.Clear();
            m_FolderEntryCache = null;
            m_AssetCollectionEntryCache = null;
            foreach (var e in m_SerializeEntries)
            {
                try
                {
                    e.parentGroup = this;
                    e.IsSubAsset = false;
                    m_EntryMap.Add(e.guid, e);
                }
                catch (Exception ex)
                {
                    Addressables.InternalSafeSerializationLog(e.address);
                    Debug.LogException(ex);
                }
            }
        }

        void OnEnable()
        {
            Validate();
        }

        internal void Validate()
        {
            bool allValid = false;
            while (!allValid)
            {
                allValid = true;
                for (int i = 0; i < m_SchemaSet.Schemas.Count; i++)
                {
                    if (m_SchemaSet.Schemas[i] == null)
                    {
                        m_SchemaSet.Schemas.RemoveAt(i);
                        allValid = false;
                        break;
                    }
                    if (m_SchemaSet.Schemas[i].Group == null)
                        m_SchemaSet.Schemas[i].Group = this;

                    m_SchemaSet.Schemas[i].Validate();
                }
            }

            var editorList = GetAssetEntry(AddressableAssetEntry.EditorSceneListName);
            if (editorList != null)
            {
                if (m_GroupName == null)
                    m_GroupName = AddressableAssetSettings.PlayerDataGroupName;
                if (m_Data != null)
                {
                    if (!HasSchema<PlayerDataGroupSchema>())
                        AddSchema<PlayerDataGroupSchema>();
                    m_Data = null;
                }
            }
            else if (m_Settings != null)
            {
                if (m_GroupName == null)
                    m_GroupName = Settings.FindUniqueGroupName("Packed Content Group");
                m_Data = null;
            }
        }

        internal void DedupeEnteries()
        {
            if (m_Settings == null)
                return;

            List<AddressableAssetEntry> removeEntries = new List<AddressableAssetEntry>();
            foreach (AddressableAssetEntry e in m_EntryMap.Values)
            {
                AddressableAssetEntry lookedUpEntry = m_Settings.FindAssetEntry(e.guid);
                if (lookedUpEntry.parentGroup != this)
                {
                    Debug.LogWarning(e.address
                        + " is already a member of group "
                        + lookedUpEntry.parentGroup
                        + " but group "
                        + m_GroupName
                        + " contained a reference to it.  Removing referece.");
                    removeEntries.Add(e);
                }
            }
            if (removeEntries.Count > 0)
                RemoveAssetEntries(removeEntries);
        }

        internal void Initialize(AddressableAssetSettings settings, string groupName, string guid, bool readOnly)
        {
            m_Settings = settings;
            m_GroupName = groupName;
            if (m_GroupName == null)
                m_GroupName = settings.FindUniqueGroupName("Packed Content Group");
            m_ReadOnly = readOnly;
            m_GUID = guid;
            m_Data = null;
        }

        /// <summary>
        /// Gathers all asset entries.  Each explicit entry may contain multiple sub entries. For example, addressable folders create entries for each asset contained within.
        /// </summary>
        /// <param name="results">The generated list of entries.  For simple entries, this will contain just the entry itself if specified.</param>
        /// <param name="includeSelf">Determines if the entry should be contained in the result list or just sub entries.</param>
        /// <param name="recurseAll">Determines if full recursion should be done when gathering entries.</param>
        /// <param name="includeSubObjects">Determines if sub objects such as sprites should be included.</param>
        /// <param name="entryFilter">Optional predicate to run against each entry, only returning those that pass.  A null filter will return all entries</param>
        public virtual void GatherAllAssets(List<AddressableAssetEntry> results, bool includeSelf, bool recurseAll, bool includeSubObjects, Func<AddressableAssetEntry, bool> entryFilter = null)
        {
            foreach (var e in entries)
                if (entryFilter == null || entryFilter(e))
                    e.GatherAllAssets(results, includeSelf, recurseAll, includeSubObjects, entryFilter);
        }

        internal virtual void GatherAllAssetReferenceDrawableEntries(List<IReferenceEntryData> results)
        {
            foreach (var e in entries)
                e.GatherAllAssetReferenceDrawableEntries(results, Settings);
        }

        internal void AddAssetEntry(AddressableAssetEntry e, bool postEvent = true)
        {
            e.IsSubAsset = false;
            e.parentGroup = this;
            m_EntryMap[e.guid] = e;
            if (m_FolderEntryCache != null && !string.IsNullOrEmpty(e.AssetPath) && e.MainAssetType == typeof(DefaultAsset) && AssetDatabase.IsValidFolder(e.AssetPath))
                m_FolderEntryCache.Add(e);
#pragma warning disable 0618
            else if (m_AssetCollectionEntryCache != null && !string.IsNullOrEmpty(e.AssetPath) && e.AssetPath.EndsWith(".asset") && e.MainAssetType == typeof(AddressableAssetEntryCollection))
                m_AssetCollectionEntryCache.Add(e);
#pragma warning restore 0618
            if (HasSchema<ContentUpdateGroupSchema>() && !GetSchema<ContentUpdateGroupSchema>().StaticContent)
                e.FlaggedDuringContentUpdateRestriction = false;
            m_SerializeEntries = null;


+            // yukimoto added.
+            SortAddress();

            SetDirty(AddressableAssetSettings.ModificationEvent.EntryAdded, e, postEvent, true);
        }

        /// <summary>
        /// Get an entry via the asset guid.
        /// </summary>
        /// <param name="guid">The asset guid.</param>
        /// <returns></returns>
        public virtual AddressableAssetEntry GetAssetEntry(string guid)
        {
            return GetAssetEntry(guid, false);
        }

        /// <summary>
        /// Get an entry via the asset guid.
        /// </summary>
        /// <param name="guid">The asset guid.</param>
        /// <param name="includeImplicit">Whether or not to include implicit asset entries in the search.</param>
        /// <returns></returns>
        public virtual AddressableAssetEntry GetAssetEntry(string guid, bool includeImplicit)
        {
            if (m_EntryMap.TryGetValue(guid, out var entry))
                return entry;
            return includeImplicit ? GetImplicitAssetEntry(guid, null) : null;
        }

        internal AddressableAssetEntry GetImplicitAssetEntry(string assetGuid, string assetPath)
        {
            if (AssetCollectionEntries.Count != 0)
            {
                AddressableAssetEntry entry;
                foreach (var e in m_AssetCollectionEntryCache)
                {
                    entry = e.GetAssetCollectionSubEntry(assetGuid);
                    if (entry != null)
                        return entry;
                }
            }
            
            if (FolderEntries.Count != 0)
            {
                if (assetPath == null)
                    assetPath = AssetDatabase.GUIDToAssetPath(assetGuid);

                AddressableAssetEntry entry;
                foreach (var e in m_FolderEntryCache)
                {
                    entry = e.GetFolderSubEntry(assetGuid, assetPath);
                    if (entry != null)
                        return entry;
                }
            }
            
            return null;
        }

        /// <summary>
        /// Marks the object as modified.
        /// </summary>
        /// <param name="modificationEvent">The event type that is changed.</param>
        /// <param name="eventData">The object data that corresponds to the event.</param>
        /// <param name="postEvent">If true, the event is propagated to callbacks.</param>
        /// <param name="groupModified">If true, the group asset will be marked as dirty.</param>
        public void SetDirty(AddressableAssetSettings.ModificationEvent modificationEvent, object eventData, bool postEvent, bool groupModified = false)
        {
            if (Settings != null)
            {
                if (groupModified && Settings.IsPersisted && this != null)
                    EditorUtility.SetDirty(this);
                Settings.SetDirty(modificationEvent, eventData, postEvent, false);
            }
        }

        /// <summary>
        /// Remove an entry.
        /// </summary>
        /// <param name="entry">The entry to remove.</param>
        /// <param name="postEvent">If true, post the event to callbacks.</param>
        public void RemoveAssetEntry(AddressableAssetEntry entry, bool postEvent = true)
        {
            m_EntryMap.Remove(entry.guid);
            m_FolderEntryCache?.Remove(entry);
            m_AssetCollectionEntryCache?.Remove(entry);
            entry.parentGroup = null;
            m_SerializeEntries = null;
            SetDirty(AddressableAssetSettings.ModificationEvent.EntryRemoved, entry, postEvent, true);
        }

        internal void RemoveAssetEntries(IEnumerable<AddressableAssetEntry> removeEntries, bool postEvent = true)
        {
            foreach (AddressableAssetEntry entry in removeEntries)
            {
                m_EntryMap.Remove(entry.guid);
                m_FolderEntryCache?.Remove(entry);
                m_AssetCollectionEntryCache?.Remove(entry);
                entry.parentGroup = null;
            }
            if (removeEntries.Count() > 0)
            {
                m_SerializeEntries = null;
                SetDirty(AddressableAssetSettings.ModificationEvent.EntryRemoved, removeEntries.ToArray(), postEvent, true);
            }
        }

        /// <summary>
        /// Check to see if a group is the Default Group.
        /// </summary>
        /// <returns></returns>
        public bool IsDefaultGroup()
        {
            return Guid == m_Settings.DefaultGroup.Guid;
        }

        /// <summary>
        /// Check if a group has the appropriate schemas and attributes that the Default Group requires.
        /// </summary>
        /// <returns></returns>
        public bool CanBeSetAsDefault()
        {
            return !m_ReadOnly;
        }

        /// <summary>
        /// Gets the index of a schema based on its specified type.
        /// </summary>
        /// <param name="type">The schema type.</param>
        /// <returns>Valid index if found, otherwise returns -1.</returns>
        public int FindSchema(Type type)
        {
            var schemas = m_SchemaSet.Schemas;
            for (int i = 0; i < schemas.Count; i++)
            {
                if (schemas[i].GetType() == type)
                {
                    return i;
                }
            }
            return -1;
        }

        private bool RenameSchemaAssets()
        {
            return m_SchemaSet.RenameSchemaAssets(GetSchemaAssetPath);
        }
    }
}

AddressableAssetSettings.cs

こちらはグループのソートを行うように追記します。

AddressableAssetSettings.cs
./Packages/com.unity.addressables@1.20.0/Editor/Settings/AddressableAssetSettings.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Formatters.Binary;
using UnityEditor.AddressableAssets.Build;
using UnityEditor.AddressableAssets.Build.DataBuilders;
using UnityEditor.AddressableAssets.HostingServices;
using UnityEditor.AddressableAssets.Settings.GroupSchemas;
using UnityEditor.Build.Pipeline.Utilities;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.AddressableAssets.ResourceLocators;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.Util;
using UnityEngine.Serialization;
using static UnityEditor.AddressableAssets.Settings.AddressablesFileEnumeration;
using System.Threading.Tasks;

#if (ENABLE_CCD && UNITY_2019_4_OR_NEWER)
using Unity.Services.Ccd.Management;
using Unity.Services.Ccd.Management.Models;
#endif

namespace UnityEditor.AddressableAssets.Settings
{
    using Object = UnityEngine.Object;

    /// <summary>
    /// Contains editor data for the addressables system.
    /// </summary>
    public class AddressableAssetSettings : ScriptableObject
    {
        internal class Cache<T1, T2>
        {
            private AddressableAssetSettings m_Settings;
            private Hash128 m_CurrentCacheVersion;
            private Dictionary<T1, T2> m_TargetInfoCache = new Dictionary<T1, T2>();

            public Cache(AddressableAssetSettings settings)
            {
                m_Settings = settings;
            }

            public bool TryGetCached(T1 key, out T2 result)
            {
                if (IsValid() && m_TargetInfoCache.TryGetValue(key, out result))
                    return true;
                
                result = default;
                return false;
            }

            public void Add(T1 key, T2 value)
            {
                if (!IsValid())
                    m_CurrentCacheVersion = m_Settings.currentHash;
                m_TargetInfoCache.Add(key, value);
            }

            private bool IsValid()
            {
                if (m_TargetInfoCache.Count > 0)
                {
                    if (!m_CurrentCacheVersion.isValid || m_CurrentCacheVersion.Equals(m_Settings.currentHash) == false)
                    {
                        m_TargetInfoCache.Clear();
                        m_CurrentCacheVersion = default;
                        return false;
                    }
                    return true;
                }
                return false;
            }
        }
        
        private Cache<string, AddressableAssetEntry> m_FindAssetEntryCache = null;
        
        [InitializeOnLoadMethod]
        static void RegisterWithAssetPostProcessor()
        {
            //if the Library folder has been deleted, this will be null and it will have to be set on the first access of the settings object
            if (AddressableAssetSettingsDefaultObject.Settings != null)
                AddressablesAssetPostProcessor.OnPostProcess.Register(AddressableAssetSettingsDefaultObject.Settings.OnPostprocessAllAssets, 0);
            else
                EditorApplication.update += TryAddAssetPostprocessorOnNextUpdate;
        }

        [InitializeOnLoadMethod]
        static void CheckCCDStatus()
        {
#if !ENABLE_CCD
            if (AddressableAssetSettingsDefaultObject.Settings != null && AddressableAssetSettingsDefaultObject.Settings.CCDEnabled)
            {
                AddressableAssetSettingsDefaultObject.Settings.CCDEnabled = false;
                Debug.LogError("This version of Addressables no longer supports integration with the current installed version of the CCD package. " +
                    "Please upgrade the CCD package to continue using the integration. Or, re-enable the Enable CCD Integration toggle in the AddressableAssetSettings.");
            }
#endif
        }

        private static void TryAddAssetPostprocessorOnNextUpdate()
        {
            if (AddressableAssetSettingsDefaultObject.Settings != null)
                AddressablesAssetPostProcessor.OnPostProcess.Register(AddressableAssetSettingsDefaultObject.Settings.OnPostprocessAllAssets, 0);
            EditorApplication.update -= TryAddAssetPostprocessorOnNextUpdate;
        }

        /// <summary>
        /// Build Path Name
        /// </summary>
        public const string kBuildPath = "BuildPath";
        /// <summary>
        /// Load Path Name
        /// </summary>
        public const string kLoadPath = "LoadPath";
        /// <summary>
        /// Default name of a newly created group.
        /// </summary>
        public const string kNewGroupName = "New Group";
        /// <summary>
        /// Default name of local build path.
        /// </summary>
        public const string kLocalBuildPath = "Local.BuildPath";
        /// <summary>
        /// Default name of local load path.
        /// </summary>
        public const string kLocalLoadPath = "Local.LoadPath";
        /// <summary>
        /// Default name of remote build path.
        /// </summary>
        public const string kRemoteBuildPath = "Remote.BuildPath";
        /// <summary>
        /// Default name of remote load path.
        /// </summary>
        public const string kRemoteLoadPath = "Remote.LoadPath";

        private const string kLocalGroupTypePrefix = "Built-In";
        internal static string LocalGroupTypePrefix => kLocalGroupTypePrefix;
        /// <summary>
        /// Default value of local build path.
        /// </summary>
        public const string kLocalBuildPathValue = "[UnityEngine.AddressableAssets.Addressables.BuildPath]/[BuildTarget]";
        /// <summary>
        /// Default value of local load path.
        /// </summary>
        public const string kLocalLoadPathValue = "{UnityEngine.AddressableAssets.Addressables.RuntimePath}/[BuildTarget]";

        private const string kEditorHostedGroupTypePrefix = "Editor Hosted";
        internal static string EditorHostedGroupTypePrefix => kEditorHostedGroupTypePrefix;
        /// <summary>
        /// Default value of remote build path.
        /// </summary>
        public const string kRemoteBuildPathValue = "ServerData/[BuildTarget]";
        /// <summary>
        /// Default value of remote load path.
        /// </summary>
        public const string kRemoteLoadPathValue = "http://localhost/[BuildTarget]";
        internal static string RemoteLoadPathValue
        {
            get
            {
                // Fix for case ADDR-2314. kRemoteLoadPathValue is incorrect, "http://localhost/[BuildTarget]" does not work with local hosting service
                return "http://[PrivateIpAddress]:[HostingServicePort]";
                // kRemoteLoadPathValue will be fixed to the correct path in Addressables 1.20.0
            }
        }

#if (ENABLE_CCD && UNITY_2019_4_OR_NEWER)
        /// <summary>
        /// Default path of build assets that are uploaded to Ccd.
        /// </summary>
        public const string kCCDBuildDataPath = "CCDBuildData";
        /// <summary>
        /// CCD Package Name
        /// </summary>
        public const string kCCDPackageName = "com.unity.services.Ccd.management";
#endif


        private const string kImportAssetEntryCollectionOptOutKey = "com.unity.addressables.importAssetEntryCollections.optOut";
        internal bool DenyEntryCollectionPermission { get; set; }

        /// <summary>
        /// Options for building Addressables when building a player.
        /// </summary>
        public enum PlayerBuildOption
        {
            /// <summary>
            /// Use to indicate that the global settings (stored in preferences) will determine if building a player will also build Addressables.
            /// </summary>
            PreferencesValue,
            /// <summary>
            /// Use to indicate that building a player will also build Addressables.
            /// </summary>
            BuildWithPlayer,
            /// <summary>
            /// Use to indicate that building a player won't build Addressables.
            /// </summary>
            DoNotBuildWithPlayer
        }

        /// <summary>
        /// Options for labeling all the different generated events.
        /// </summary>
        public enum ModificationEvent
        {
            /// <summary>
            /// Use to indicate that a group was added to the settings object.
            /// </summary>
            GroupAdded,
            /// <summary>
            /// Use to indicate that a group was removed from the the settings object.
            /// </summary>
            GroupRemoved,
            /// <summary>
            /// Use to indicate that a group in the settings object was renamed.
            /// </summary>
            GroupRenamed,
            /// <summary>
            /// Use to indicate that a schema was added to a group.
            /// </summary>
            GroupSchemaAdded,
            /// <summary>
            /// Use to indicate that a schema was removed from a group.
            /// </summary>
            GroupSchemaRemoved,
            /// <summary>
            /// Use to indicate that a schema was modified.
            /// </summary>
            GroupSchemaModified,
            /// <summary>
            /// Use to indicate that a group template was added to the settings object.
            /// </summary>
            GroupTemplateAdded,
            /// <summary>
            /// Use to indicate that a group template was removed from the settings object.
            /// </summary>
            GroupTemplateRemoved,
            /// <summary>
            /// Use to indicate that a schema was added to a group template.
            /// </summary>
            GroupTemplateSchemaAdded,
            /// <summary>
            /// Use to indicate that a schema was removed from a group template.
            /// </summary>
            GroupTemplateSchemaRemoved,
            /// <summary>
            /// Use to indicate that an asset entry was created.
            /// </summary>
            EntryCreated,
            /// <summary>
            /// Use to indicate that an asset entry was added to a group.
            /// </summary>
            EntryAdded,
            /// <summary>
            /// Use to indicate that an asset entry moved from one group to another.
            /// </summary>
            EntryMoved,
            /// <summary>
            /// Use to indicate that an asset entry was removed from a group.
            /// </summary>
            EntryRemoved,
            /// <summary>
            /// Use to indicate that an asset label was added to the settings object.
            /// </summary>
            LabelAdded,
            /// <summary>
            /// Use to indicate that an asset label was removed from the settings object.
            /// </summary>
            LabelRemoved,
            /// <summary>
            /// Use to indicate that a profile was added to the settings object.
            /// </summary>
            ProfileAdded,
            /// <summary>
            /// Use to indicate that a profile was removed from the settings object.
            /// </summary>
            ProfileRemoved,
            /// <summary>
            /// Use to indicate that a profile was modified.
            /// </summary>
            ProfileModified,
            /// <summary>
            /// Use to indicate that a profile has been set as the active profile.
            /// </summary>
            ActiveProfileSet,
            /// <summary>
            /// Use to indicate that an asset entry was modified.
            /// </summary>
            EntryModified,
            /// <summary>
            /// Use to indicate that the build settings object was modified.
            /// </summary>
            BuildSettingsChanged,
            /// <summary>
            /// Use to indicate that a new build script is being used as the active build script.
            /// </summary>
            ActiveBuildScriptChanged,
            /// <summary>
            /// Use to indicate that a new data builder script was added to the settings object.
            /// </summary>
            DataBuilderAdded,
            /// <summary>
            /// Use to indicate that a data builder script was removed from the settings object.
            /// </summary>
            DataBuilderRemoved,
            /// <summary>
            /// Use to indicate a new initialization object was added to the settings object.
            /// </summary>
            InitializationObjectAdded,
            /// <summary>
            /// Use to indicate a initialization object was removed from the settings object.
            /// </summary>
            InitializationObjectRemoved,
            /// <summary>
            /// Use to indicate that a new script is being used as the active playmode data builder.
            /// </summary>
            ActivePlayModeScriptChanged,
            /// <summary>
            /// Use to indicate that a batch of asset entries was modified. Note that the posted object will be null.
            /// </summary>
            BatchModification,
            /// <summary>
            /// Use to indicate that the hosting services manager was modified.
            /// </summary>
            HostingServicesManagerModified,
            /// <summary>
            /// Use to indicate that a group changed its order placement within the list of groups in the settings object.
            /// </summary>
            GroupMoved,
            /// <summary>
            /// Use to indicate that a new certificate handler is being used for the initialization object provider.
            /// </summary>
            CertificateHandlerChanged
        }

        /// <summary>
        /// The path of the settings asset.
        /// </summary>
        public string AssetPath
        {
            get
            {
                string guid;
                long localId;
                if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(this, out guid, out localId))
                    throw new Exception($"{nameof(AddressableAssetSettings)} is not persisted.  Unable to determine AssetPath.");
                var assetPath = AssetDatabase.GUIDToAssetPath(guid);
                if (string.IsNullOrEmpty(assetPath))
                    throw new Exception($"{nameof(AddressableAssetSettings)} - Unable to determine AssetPath from guid {guid}.");
                return assetPath;
            }
        }

        /// <summary>
        /// The folder of the settings asset.
        /// </summary>
        public string ConfigFolder
        {
            get
            {
                return Path.GetDirectoryName(AssetPath);
            }
        }

        /// <summary>
        /// The folder for the group assets.
        /// </summary>
        public string GroupFolder
        {
            get
            {
                return ConfigFolder + "/AssetGroups";
            }
        }
        /// <summary>
        /// The folder for the script assets.
        /// </summary>
        public string DataBuilderFolder
        {
            get
            {
                return ConfigFolder + "/DataBuilders";
            }
        }
        /// <summary>
        /// The folder for the asset group schema assets.
        /// </summary>
        public string GroupSchemaFolder
        {
            get
            {
                return GroupFolder + "/Schemas";
            }
        }

        /// <summary>
        /// The default folder for the group template assets.
        /// </summary>
        public string GroupTemplateFolder
        {
            get
            {
                return ConfigFolder + "/AssetGroupTemplates";
            }
        }

        /// <summary>
        /// Event for handling settings changes.  The object passed depends on the event type.
        /// </summary>
        public Action<AddressableAssetSettings, ModificationEvent, object> OnModification { get; set; }

        /// <summary>
        /// Event for handling settings changes on all instances of AddressableAssetSettings.  The object passed depends on the event type.
        /// </summary>
        public static event Action<AddressableAssetSettings, ModificationEvent, object> OnModificationGlobal;

        /// <summary>
        /// Event for handling the result of a DataBuilder.Build call.
        /// </summary>
        public Action<AddressableAssetSettings, IDataBuilder, IDataBuilderResult> OnDataBuilderComplete { get; set; }

        [FormerlySerializedAs("m_defaultGroup")]
        [SerializeField]
        string m_DefaultGroup;
        [FormerlySerializedAs("m_cachedHash")]
        [SerializeField]
        Hash128 m_CachedHash;

        bool m_IsTemporary;
        /// <summary>
        /// Returns whether this settings object is persisted to an asset.
        /// </summary>
        public bool IsPersisted { get { return !m_IsTemporary; } }

        [SerializeField]
        bool m_OptimizeCatalogSize = false;

        [SerializeField]
        bool m_BuildRemoteCatalog = false;

        [SerializeField]
        bool m_BundleLocalCatalog = false;

        [SerializeField]
        int m_CatalogRequestsTimeout = 0;

        [SerializeField]
        bool m_DisableCatalogUpdateOnStart = false;

        [SerializeField]
        bool m_IgnoreUnsupportedFilesInBuild = false;

        [SerializeField]
        bool m_UniqueBundleIds = false;

        [SerializeField]
#if UNITY_2021_1_OR_NEWER
        bool m_NonRecursiveBuilding = true;
#else
        bool m_NonRecursiveBuilding = false;
#endif

#if UNITY_2019_4_OR_NEWER
        [SerializeField]
#if !ENABLE_CCD
        bool m_CCDEnabled = false;
#else
        bool m_CCDEnabled = true;
#endif

        public bool CCDEnabled
        {
            get { return m_CCDEnabled; }
            set { m_CCDEnabled = value; }
        }
#endif

        [SerializeField]
        int m_maxConcurrentWebRequests = 3;

        /// <summary>
        /// The maximum time to download hash and json catalog files before a timeout error.
        /// </summary>
        public int CatalogRequestsTimeout
        {
            get { return m_CatalogRequestsTimeout; }
            set { m_CatalogRequestsTimeout = value < 0 ? 0 : value; }
        }

        /// <summary>
        /// The maximum number of concurrent web requests.  This value will be clamped from 1 to 1024.
        /// </summary>
        public int MaxConcurrentWebRequests
        {
            get { return m_maxConcurrentWebRequests; }
            set { m_maxConcurrentWebRequests = Mathf.Clamp(value, 1, 1024); }
        }

        /// <summary>
        /// Set this to true to ensure unique bundle ids. Set to false to allow duplicate bundle ids.
        /// </summary>
        public bool UniqueBundleIds
        {
            get { return m_UniqueBundleIds; }
            set { m_UniqueBundleIds = value; }
        }

        [SerializeField]
#if UNITY_2021_1_OR_NEWER
        bool m_ContiguousBundles = true;
#else
        bool m_ContiguousBundles = false;
#endif

        /// <summary>
        /// If set, packs assets in bundles contiguously based on the ordering of the source asset which results in improved asset loading times. Disable this if you've built bundles with a version of Addressables older than 1.12.1 and you want to minimize bundle changes.
        /// </summary>
        public bool ContiguousBundles
        {
            get { return m_ContiguousBundles; }
            set { m_ContiguousBundles = value; }
        }

        /// <summary>
        /// If set, Calculates and build asset bundles using Non-Recursive Dependency calculation methods. This approach helps reduce asset bundle rebuilds and runtime memory consumption.
        /// </summary>
        public bool NonRecursiveBuilding
        {
            get { return m_NonRecursiveBuilding; }
            set { m_NonRecursiveBuilding = value; }
        }

        /// <summary>
        /// Enables size optimization of content catalogs.  This may increase the cpu usage of loading the catalog.
        /// </summary>
        public bool OptimizeCatalogSize
        {
            get { return m_OptimizeCatalogSize; }
            set { m_OptimizeCatalogSize = value; }
        }

        /// <summary>
        /// Determine if a remote catalog should be built-for and loaded-by the app.
        /// </summary>
        public bool BuildRemoteCatalog
        {
            get { return m_BuildRemoteCatalog; }
            set { m_BuildRemoteCatalog = value; }
        }

        /// <summary>
        /// Whether the local catalog should be serialized in an asset bundle or as json.
        /// </summary>
        public bool BundleLocalCatalog
        {
            get { return m_BundleLocalCatalog; }
            set { m_BundleLocalCatalog = value; }
        }

        /// <summary>
        /// Tells Addressables if it should check for a Content Catalog Update during the initialization step.
        /// </summary>
        public bool DisableCatalogUpdateOnStartup
        {
            get { return m_DisableCatalogUpdateOnStart; }
            set { m_DisableCatalogUpdateOnStart = value; }
        }

        [SerializeField]
        bool m_StripUnityVersionFromBundleBuild = false;
        /// <summary>
        /// If true, this option will strip the Unity Editor Version from the header of the AssetBundle during a build.
        /// </summary>
        internal bool StripUnityVersionFromBundleBuild
        {
            get { return m_StripUnityVersionFromBundleBuild; }
            set { m_StripUnityVersionFromBundleBuild = value; }
        }

        [SerializeField]
        bool m_DisableVisibleSubAssetRepresentations = false;
        /// <summary>
        /// If true, the build will assume that sub Assets have no visible asset representations (are not visible in the Project view) which results in improved build times.
        /// However sub assets in the built bundles cannot be accessed by AssetBundle.LoadAsset&lt;T&gt; or AssetBundle.LoadAllAssets&lt;T&gt;.
        /// </summary>
        public bool DisableVisibleSubAssetRepresentations
        {
            get { return m_DisableVisibleSubAssetRepresentations; }
            set { m_DisableVisibleSubAssetRepresentations = value; }
        }

        /// <summary>
        /// Whether unsupported files during build should be ignored or treated as an error.
        /// </summary>
        public bool IgnoreUnsupportedFilesInBuild
        {
            get { return m_IgnoreUnsupportedFilesInBuild; }
            set { m_IgnoreUnsupportedFilesInBuild = value; }
        }

        [SerializeField]
        ShaderBundleNaming m_ShaderBundleNaming = ShaderBundleNaming.ProjectName;
        /// <summary>
        /// Sets the naming convention used for the Unity built in shader bundle at build time.
        /// The recommended setting is Project Name.
        /// </summary>
        public ShaderBundleNaming ShaderBundleNaming
        {
            get { return m_ShaderBundleNaming; }
            set { m_ShaderBundleNaming = value; }
        }

        [SerializeField]
        string m_ShaderBundleCustomNaming = "";
        /// <summary>
        /// Custom Unity built in shader bundle prefix that is used if AddressableAssetSettings.ShaderBundleNaming is set to ShaderBundleNaming.Custom.
        /// </summary>
        public string ShaderBundleCustomNaming
        {
            get { return m_ShaderBundleCustomNaming; }
            set { m_ShaderBundleCustomNaming = value; }
        }

        [SerializeField]
        MonoScriptBundleNaming m_MonoScriptBundleNaming = MonoScriptBundleNaming.Disabled;
        /// <summary>
        /// Sets the naming convention used for the MonoScript bundle at build time. Or disabled MonoScript bundle generation.
        /// The recommended setting is Project Name.
        /// </summary>
        public MonoScriptBundleNaming MonoScriptBundleNaming
        {
            get { return m_MonoScriptBundleNaming; }
            set { m_MonoScriptBundleNaming = value; }
        }

        [SerializeField]
        CheckForContentUpdateRestrictionsOptions m_CheckForContentUpdateRestrictionsOption = CheckForContentUpdateRestrictionsOptions.ListUpdatedAssetsWithRestrictions;

        /// <summary>
        /// Informs the Addressable system how to handle checking for Content Update Restrictions during a Content Update build.
        /// During this check, assets are flagged that have changed, yet are contained in a Group that has the Cannot Change Post Release option set.
        /// </summary>
        public CheckForContentUpdateRestrictionsOptions CheckForContentUpdateRestrictionsOption
        {
            get { return m_CheckForContentUpdateRestrictionsOption; }
            set { m_CheckForContentUpdateRestrictionsOption = value; }
        }

#if ENABLE_CCD
        [SerializeField]
        BuildAndReleaseContentStateBehavior m_BuildAndReleaseBinFileOption = 0;

        /// <summary>
        /// Informs the Addressable system how to handle checking for Content Update Restrictions during a Content Update build.
        /// During this check, assets are flagged that have changed, yet are contained in a Group that has the Cannot Change Post Release option set.
        /// </summary>
        public BuildAndReleaseContentStateBehavior BuildAndReleaseBinFileOption
        {
            get { return m_BuildAndReleaseBinFileOption; }
            set { m_BuildAndReleaseBinFileOption = value; }
        }
#endif

        [SerializeField]
        string m_MonoScriptBundleCustomNaming = "";
        /// <summary>
        /// Custom MonoScript bundle prefix that is used if AddressableAssetSettings.MonoScriptBundleNaming is set to MonoScriptBundleNaming.Custom.
        /// </summary>
        public string MonoScriptBundleCustomNaming
        {
            get { return m_MonoScriptBundleCustomNaming; }
            set { m_MonoScriptBundleCustomNaming = value; }
        }

        [SerializeField]
        ProfileValueReference m_RemoteCatalogBuildPath;
        /// <summary>
        /// The path to place a copy of the content catalog for online retrieval.  To do any content updates
        /// to an existing built app, there must be a remote catalog. Overwriting the catalog is how the app
        /// gets informed of the updated content.
        /// </summary>
        public ProfileValueReference RemoteCatalogBuildPath
        {
            get
            {
                if (m_RemoteCatalogBuildPath.Id == null)
                {
                    m_RemoteCatalogBuildPath = new ProfileValueReference();
                    m_RemoteCatalogBuildPath.SetVariableByName(this, kRemoteBuildPath);
                }
                return m_RemoteCatalogBuildPath;
            }
            set { m_RemoteCatalogBuildPath = value; }
        }

        [SerializeField]
        ProfileValueReference m_RemoteCatalogLoadPath;
        /// <summary>
        /// The path to load the remote content catalog from.  This is the location the app will check to
        /// look for updated catalogs, which is the only indication the app has for updated content.
        /// </summary>
        public ProfileValueReference RemoteCatalogLoadPath
        {
            get
            {
                if (m_RemoteCatalogLoadPath.Id == null)
                {
                    m_RemoteCatalogLoadPath = new ProfileValueReference();
                    m_RemoteCatalogLoadPath.SetVariableByName(this, kRemoteLoadPath);
                }
                return m_RemoteCatalogLoadPath;
            }
            set { m_RemoteCatalogLoadPath = value; }
        }

        [SerializeField]
        internal string m_ContentStateBuildPathProfileVariableName = "";

        [SerializeField]
        internal string m_CustomContentStateBuildPath = "";

        [SerializeField]
        internal string m_ContentStateBuildPath = "";
        /// <summary>
        /// The path used for saving the addressables_content_state.bin file.  If empty, this will be the addressable settings config folder in your project.
        /// </summary>
        public string ContentStateBuildPath
        {
            get 
            {
                if (!string.IsNullOrEmpty(m_ContentStateBuildPath))
                    return m_ContentStateBuildPath;
                else if (m_ContentStateBuildPathProfileVariableName == AddressableAssetProfileSettings.customEntryString)
                    return m_CustomContentStateBuildPath;
                else if (m_ContentStateBuildPathProfileVariableName == AddressableAssetProfileSettings.defaultSettingsPathString)
                    return $"{AddressableAssetSettingsDefaultObject.kDefaultConfigFolder}/{PlatformMappingService.GetPlatformPathSubFolder()}";
                else
                    return profileSettings.GetValueByName(activeProfileId, m_ContentStateBuildPathProfileVariableName);
            }
            set 
            { 
                m_ContentStateBuildPath = value;
                m_CustomContentStateBuildPath = value;
            }
        }

        [SerializeField]
        private PlayerBuildOption m_BuildAddressablesWithPlayerBuild = PlayerBuildOption.DoNotBuildWithPlayer;

        /// <summary>
        /// Defines if Addressables content will be built along with a Player build. (Requires 2021.2 or above)
        /// </summary>
        /// <remarks>
        /// Build with Player, will build Addressables with a Player build, this overrides preferences value.
        /// Do not Build with Player, will not build Addressables with a Player build, this overrides preferences value.
        /// Preferences value, will build with the Player dependant on is the user preferences value for "Build Addressables on Player build" is set.
        /// </remarks>
        public PlayerBuildOption BuildAddressablesWithPlayerBuild
        {
            get { return m_BuildAddressablesWithPlayerBuild; }
            set { m_BuildAddressablesWithPlayerBuild = value; }
        }

        internal string GetContentStateBuildPath()
        {
            string p = ConfigFolder;
            if (!string.IsNullOrEmpty(m_ContentStateBuildPath))
                p = m_ContentStateBuildPath;
            p = Path.Combine(p, PlatformMappingService.GetPlatformPathSubFolder());
            return p;
        }

        /// <summary>
        /// Hash of the current settings.  This value is recomputed if anything changes.
        /// </summary>
        public Hash128 currentHash
        {
            get
            {
                if (m_CachedHash.isValid)
                    return m_CachedHash;
                var stream = new MemoryStream();
                var formatter = new BinaryFormatter();
                m_BuildSettings.SerializeForHash(formatter, stream);
                formatter.Serialize(stream, activeProfileId);
                formatter.Serialize(stream, m_LabelTable);
                formatter.Serialize(stream, m_ProfileSettings);
                formatter.Serialize(stream, m_GroupAssets.Count);
                foreach (var g in m_GroupAssets)
                    g.SerializeForHash(formatter, stream);
                return (m_CachedHash = HashingMethods.Calculate(stream).ToHash128());
            }
        }

        internal void DataBuilderCompleted(IDataBuilder builder, IDataBuilderResult result)
        {
            if (OnDataBuilderComplete != null)
                OnDataBuilderComplete(this, builder, result);
        }

        /// <summary>
        /// Create an AssetReference object.  If the asset is not already addressable, it will be added.
        /// </summary>
        /// <param name="guid">The guid of the asset reference.</param>
        /// <returns>Returns the newly created AssetReference.</returns>
        public AssetReference CreateAssetReference(string guid)
        {
            CreateOrMoveEntry(guid, DefaultGroup);
            return new AssetReference(guid);
        }

        [SerializeField]
        string m_overridePlayerVersion = "";
        /// <summary>
        /// Allows for overriding the player version used to generated catalog names.
        /// </summary>
        public string OverridePlayerVersion
        {
            get { return m_overridePlayerVersion; }
            set { m_overridePlayerVersion = value; }
        }
        /// <summary>
        /// The version of the player build.  This is implemented as a timestamp int UTC of the form  string.Format("{0:D4}.{1:D2}.{2:D2}.{3:D2}.{4:D2}.{5:D2}", now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second).
        /// </summary>
        public string PlayerBuildVersion
        {
            get
            {
                if (!string.IsNullOrEmpty(m_overridePlayerVersion))
                    return profileSettings.EvaluateString(activeProfileId, m_overridePlayerVersion);
                var now = DateTime.UtcNow;
                return string.Format("{0:D4}.{1:D2}.{2:D2}.{3:D2}.{4:D2}.{5:D2}", now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second);
            }
        }

        [FormerlySerializedAs("m_groupAssets")]
        [SerializeField]
        List<AddressableAssetGroup> m_GroupAssets = new List<AddressableAssetGroup>();
        /// <summary>
        /// List of asset groups.
        /// </summary>
        public List<AddressableAssetGroup> groups { get { return m_GroupAssets; } }

        [FormerlySerializedAs("m_buildSettings")]
        [SerializeField]
        AddressableAssetBuildSettings m_BuildSettings = new AddressableAssetBuildSettings();
        /// <summary>
        /// Build settings object.
        /// </summary>
        public AddressableAssetBuildSettings buildSettings { get { return m_BuildSettings; } }

        [FormerlySerializedAs("m_profileSettings")]
        [SerializeField]
        AddressableAssetProfileSettings m_ProfileSettings = new AddressableAssetProfileSettings();
        /// <summary>
        /// Profile settings object.
        /// </summary>
        public AddressableAssetProfileSettings profileSettings { get { return m_ProfileSettings; } }

        [FormerlySerializedAs("m_labelTable")]
        [SerializeField]
        LabelTable m_LabelTable = new LabelTable();
        /// <summary>
        /// LabelTable object.
        /// </summary>
        internal LabelTable labelTable { get { return m_LabelTable; } }
        [FormerlySerializedAs("m_schemaTemplates")]
        [SerializeField]
        List<AddressableAssetGroupSchemaTemplate> m_SchemaTemplates = new List<AddressableAssetGroupSchemaTemplate>();

        /// <summary>
        /// Remove  the schema at the specified index.
        /// </summary>
        /// <param name="index">The index to remove at.</param>
        /// <param name="postEvent">Indicates if an even should be posted to the Addressables event system for this change.</param>
        /// <returns>True if the schema was removed.</returns>
        [Obsolete("GroupSchemaTemplates are deprecated, use GroupTemplateObjects")]
        public bool RemoveSchemaTemplate(int index, bool postEvent = true)
        {
            Debug.LogError("GroupSchemaTemplates are deprecated, use GroupTemplateObjects");
            return false;
        }

        [SerializeField]
        List<ScriptableObject> m_GroupTemplateObjects = new List<ScriptableObject>();

        /// <summary>
        /// List of ScriptableObjects that implement the IGroupTemplate interface for providing new templates.
        /// For use in the AddressableAssetsWindow to display new groups to create
        /// </summary>
        public List<ScriptableObject> GroupTemplateObjects
        {
            get { return m_GroupTemplateObjects; }
        }

        /// <summary>
        /// Get the IGroupTemplate at the specified index.
        /// </summary>
        /// <param name="index">The index of the template object.</param>
        /// <returns>The AddressableAssetGroupTemplate object at the specified index.</returns>
        public IGroupTemplate GetGroupTemplateObject(int index)
        {
            if (m_GroupTemplateObjects.Count == 0)
                return null;
            if (index < 0 || index >= m_GroupTemplateObjects.Count)
            {
                Debug.LogWarningFormat("Invalid index for group template: {0}.", index);
                return null;
            }
            return m_GroupTemplateObjects[Mathf.Clamp(index, 0, m_GroupTemplateObjects.Count)] as IGroupTemplate;
        }

        /// <summary>
        /// Adds a AddressableAssetsGroupTemplate object.
        /// </summary>
        /// <param name="templateObject">The AddressableAssetGroupTemplate object to add.</param>
        /// <param name="postEvent">Indicates if an even should be posted to the Addressables event system for this change.</param>
        /// <returns>True if the initialization object was added.</returns>
        public bool AddGroupTemplateObject(IGroupTemplate templateObject, bool postEvent = true)
        {
            if (templateObject == null)
            {
                Debug.LogWarning("Cannot add null IGroupTemplate");
                return false;
            }
            var so = templateObject as ScriptableObject;
            if (so == null)
            {
                Debug.LogWarning("Group Template objects must inherit from ScriptableObject.");
                return false;
            }

            m_GroupTemplateObjects.Add(so);
            SetDirty(ModificationEvent.GroupTemplateAdded, so, postEvent, true);
            return true;
        }

        /// <summary>
        /// Remove the AddressableAssetGroupTemplate object at the specified index.
        /// </summary>
        /// <param name="index">The index to remove.</param>
        /// <param name="postEvent">Indicates if an event should be posted to the Addressables event system for this change.</param>
        /// <returns>True if the initialization object was removed.</returns>
        public bool RemoveGroupTemplateObject(int index, bool postEvent = true)
        {
            if (m_GroupTemplateObjects.Count <= index)
                return false;
            var so = m_GroupTemplateObjects[index];
            m_GroupTemplateObjects.RemoveAt(index);
            SetDirty(ModificationEvent.GroupTemplateRemoved, so, postEvent, true);
            return true;
        }

        /// <summary>
        /// Sets the initialization object at the specified index.
        /// </summary>
        /// <param name="index">The index to set the initialization object.</param>
        /// <param name="templateObject">The rroup template object to set.  This must be a valid scriptable object that implements the IGroupTemplate interface.</param>
        /// <param name="postEvent">Indicates if an even should be posted to the Addressables event system for this change.</param>
        /// <returns>True if the initialization object was set, false otherwise.</returns>
        public bool SetGroupTemplateObjectAtIndex(int index, IGroupTemplate templateObject, bool postEvent = true)
        {
            if (m_GroupTemplateObjects.Count <= index)
                return false;
            if (templateObject == null)
            {
                Debug.LogWarning("Cannot set null IGroupTemplate");
                return false;
            }
            var so = templateObject as ScriptableObject;
            if (so == null)
            {
                Debug.LogWarning("AddressableAssetGroupTemplate objects must inherit from ScriptableObject.");
                return false;
            }

            m_GroupTemplateObjects[index] = so;
            SetDirty(ModificationEvent.GroupTemplateAdded, so, postEvent, true);
            return true;
        }

        [FormerlySerializedAs("m_initializationObjects")]
        [SerializeField]
        List<ScriptableObject> m_InitializationObjects = new List<ScriptableObject>();
        /// <summary>
        /// List of ScriptableObjects that implement the IObjectInitializationDataProvider interface for providing runtime initialization.
        /// </summary>
        public List<ScriptableObject> InitializationObjects
        {
            get { return m_InitializationObjects; }
        }

        /// <summary>
        /// Get the IObjectInitializationDataProvider at a specifc index.
        /// </summary>
        /// <param name="index">The index of the initialization object.</param>
        /// <returns>The initialization object at the specified index.</returns>
        public IObjectInitializationDataProvider GetInitializationObject(int index)
        {
            if (m_InitializationObjects.Count == 0)
                return null;
            if (index < 0 || index >= m_InitializationObjects.Count)
            {
                Debug.LogWarningFormat("Invalid index for data builder: {0}.", index);
                return null;
            }
            return m_InitializationObjects[Mathf.Clamp(index, 0, m_InitializationObjects.Count)] as IObjectInitializationDataProvider;
        }

        /// <summary>
        /// Adds an initialization object.
        /// </summary>
        /// <param name="initObject">The initialization object to add.</param>
        /// <param name="postEvent">Indicates if an even should be posted to the Addressables event system for this change.</param>
        /// <returns>True if the initialization object was added.</returns>
        public bool AddInitializationObject(IObjectInitializationDataProvider initObject, bool postEvent = true)
        {
            if (initObject == null)
            {
                Debug.LogWarning("Cannot add null IObjectInitializationDataProvider");
                return false;
            }
            var so = initObject as ScriptableObject;
            if (so == null)
            {
                Debug.LogWarning("Initialization objects must inherit from ScriptableObject.");
                return false;
            }

            m_InitializationObjects.Add(so);
            SetDirty(ModificationEvent.InitializationObjectAdded, so, postEvent, true);
            return true;
        }

        /// <summary>
        /// Remove the initialization object at the specified index.
        /// </summary>
        /// <param name="index">The index to remove.</param>
        /// <param name="postEvent">Indicates if an even should be posted to the Addressables event system for this change.</param>
        /// <returns>True if the initialization object was removed.</returns>
        public bool RemoveInitializationObject(int index, bool postEvent = true)
        {
            if (m_InitializationObjects.Count <= index)
                return false;
            var so = m_InitializationObjects[index];
            m_InitializationObjects.RemoveAt(index);
            SetDirty(ModificationEvent.InitializationObjectRemoved, so, postEvent, true);
            return true;
        }

        /// <summary>
        /// Sets the initialization object at the specified index.
        /// </summary>
        /// <param name="index">The index to set the initialization object.</param>
        /// <param name="initObject">The initialization object to set.  This must be a valid scriptable object that implements the IInitializationObject interface.</param>
        /// <param name="postEvent">Indicates if an even should be posted to the Addressables event system for this change.</param>
        /// <returns>True if the initialization object was set, false otherwise.</returns>
        public bool SetInitializationObjectAtIndex(int index, IObjectInitializationDataProvider initObject, bool postEvent = true)
        {
            if (m_InitializationObjects.Count <= index)
                return false;
            if (initObject == null)
            {
                Debug.LogWarning("Cannot add null IObjectInitializationDataProvider");
                return false;
            }
            var so = initObject as ScriptableObject;
            if (so == null)
            {
                Debug.LogWarning("Initialization objects must inherit from ScriptableObject.");
                return false;
            }

            m_InitializationObjects[index] = so;
            SetDirty(ModificationEvent.InitializationObjectAdded, so, postEvent, true);
            return true;
        }

        [SerializeField]
        [SerializedTypeRestriction(type = typeof(UnityEngine.Networking.CertificateHandler))]
        SerializedType m_CertificateHandlerType;

        /// <summary>
        /// The type of CertificateHandler to use for this provider.
        /// </summary>
        public Type CertificateHandlerType
        {
            get
            {
                return m_CertificateHandlerType.Value;
            }
            set
            {
                m_CertificateHandlerType.Value = value;
                SetDirty(ModificationEvent.CertificateHandlerChanged, value, true, true);
            }
        }

        [FormerlySerializedAs("m_activePlayerDataBuilderIndex")]
        [SerializeField]
        int m_ActivePlayerDataBuilderIndex = 3;
        [FormerlySerializedAs("m_dataBuilders")]
        [SerializeField]
        List<ScriptableObject> m_DataBuilders = new List<ScriptableObject>();
        /// <summary>
        /// List of ScriptableObjects that implement the IDataBuilder interface.  These are used to create data for editor play mode and for player builds.
        /// </summary>
        public List<ScriptableObject> DataBuilders { get { return m_DataBuilders; } }
        /// <summary>
        /// Get The data builder at a specifc index.
        /// </summary>
        /// <param name="index">The index of the builder.</param>
        /// <returns>The data builder at the specified index.</returns>
        public IDataBuilder GetDataBuilder(int index)
        {
            if (m_DataBuilders.Count == 0)
                return null;
            if (index < 0 || index >= m_DataBuilders.Count)
            {
                Debug.LogWarningFormat("Invalid index for data builder: {0}.", index);
                return null;
            }
            return m_DataBuilders[Mathf.Clamp(index, 0, m_DataBuilders.Count)] as IDataBuilder;
        }

        /// <summary>
        /// Adds a data builder.
        /// </summary>
        /// <param name="builder">The data builder to add.</param>
        /// <param name="postEvent">Indicates if an even should be posted to the Addressables event system for this change.</param>
        /// <returns>True if the data builder was added.</returns>
        public bool AddDataBuilder(IDataBuilder builder, bool postEvent = true)
        {
            if (builder == null)
            {
                Debug.LogWarning("Cannot add null IDataBuilder");
                return false;
            }
            var so = builder as ScriptableObject;
            if (so == null)
            {
                Debug.LogWarning("Data builders must inherit from ScriptableObject.");
                return false;
            }

            m_DataBuilders.Add(so);
            SetDirty(ModificationEvent.DataBuilderAdded, so, postEvent, true);
            return true;
        }

        /// <summary>
        /// Remove the data builder at the sprcified index.
        /// </summary>
        /// <param name="index">The index to remove.</param>
        /// <param name="postEvent">Indicates if an even should be posted to the Addressables event system for this change.</param>
        /// <returns>True if the builder was removed.</returns>
        public bool RemoveDataBuilder(int index, bool postEvent = true)
        {
            if (m_DataBuilders.Count <= index)
                return false;
            var so = m_DataBuilders[index];
            m_DataBuilders.RemoveAt(index);
            SetDirty(ModificationEvent.DataBuilderRemoved, so, postEvent, true);
            return true;
        }

        /// <summary>
        /// Sets the data builder at the specified index.
        /// </summary>
        /// <param name="index">The index to set the builder.</param>
        /// <param name="builder">The builder to set.  This must be a valid scriptable object that implements the IDataBuilder interface.</param>
        /// <param name="postEvent">Indicates if an even should be posted to the Addressables event system for this change.</param>
        /// <returns>True if the builder was set, false otherwise.</returns>
        public bool SetDataBuilderAtIndex(int index, IDataBuilder builder, bool postEvent = true)
        {
            if (m_DataBuilders.Count <= index)
                return false;
            if (builder == null)
            {
                Debug.LogWarning("Cannot add null IDataBuilder");
                return false;
            }
            var so = builder as ScriptableObject;
            if (so == null)
            {
                Debug.LogWarning("Data builders must inherit from ScriptableObject.");
                return false;
            }

            m_DataBuilders[index] = so;
            SetDirty(ModificationEvent.DataBuilderAdded, so, postEvent, true);
            return true;
        }

        /// <summary>
        /// Get the active data builder for player data.
        /// </summary>
        public IDataBuilder ActivePlayerDataBuilder
        {
            get
            {
                return GetDataBuilder(m_ActivePlayerDataBuilderIndex);
            }
        }

        /// <summary>
        /// Get the active data builder for editor play mode data.
        /// </summary>
        public IDataBuilder ActivePlayModeDataBuilder
        {
            get
            {
                return GetDataBuilder(ProjectConfigData.ActivePlayModeIndex);
            }
        }

        /// <summary>
        /// Get the index of the active player data builder.
        /// </summary>
        public int ActivePlayerDataBuilderIndex
        {
            get
            {
                return m_ActivePlayerDataBuilderIndex;
            }
            set
            {
                if (m_ActivePlayerDataBuilderIndex != value)
                {
                    m_ActivePlayerDataBuilderIndex = value;
                    SetDirty(ModificationEvent.ActiveBuildScriptChanged, ActivePlayerDataBuilder, true, true);
                }
            }
        }

        /// <summary>
        /// Get the index of the active play mode data builder.
        /// </summary>
        public int ActivePlayModeDataBuilderIndex
        {
            get
            {
                return ProjectConfigData.ActivePlayModeIndex;
            }
            set
            {
                ProjectConfigData.ActivePlayModeIndex = value;
                SetDirty(ModificationEvent.ActivePlayModeScriptChanged, ActivePlayModeDataBuilder, true, false);
            }
        }

        /// <summary>
        /// Gets the list of all defined labels.
        /// </summary>
        /// <returns>Returns a list of all defined labels.</returns>
        public List<string> GetLabels()
        {
            return m_LabelTable.labelNames.ToList();
        }

        /// <summary>
        /// Add a new label.
        /// </summary>
        /// <param name="label">The label name.</param>
        /// <param name="postEvent">Send modification event.</param>
        public void AddLabel(string label, bool postEvent = true)
        {
            if (m_LabelTable.AddLabelName(label))
                SetDirty(ModificationEvent.LabelAdded, label, postEvent, true);
        }

        internal void RenameLabel(string oldLabelName, string newLabelName)
        {
            int index = m_LabelTable.GetIndexOfLabel(oldLabelName);
            if (index < 0)
                return;

            if (!m_LabelTable.AddLabelName(newLabelName, index))
                return;

            foreach (var group in groups)
            {
                foreach (var entry in group.entries)
                {
                    if (entry.labels.Contains(oldLabelName))
                    {
                        entry.labels.Remove(oldLabelName);
                        entry.SetLabel(newLabelName, true);
                    }
                }
            }

            m_LabelTable.RemoveLabelName(oldLabelName);
        }

        /// <summary>
        /// Remove a label by name.
        /// </summary>
        /// <param name="label">The label name.</param>
        /// <param name="postEvent">Send modification event.</param>
        public void RemoveLabel(string label, bool postEvent = true)
        {
            m_LabelTable.RemoveLabelName(label);
            SetDirty(ModificationEvent.LabelRemoved, label, postEvent, true);
            Debug.LogWarningFormat("Label \"{0}\" removed. If you re-add the label before building, it will be restored in entries that had it. " +
                "Building Addressables content will clear this label from all entries. That action cannot be undone.", label);
        }

        [FormerlySerializedAs("m_activeProfileId")]
        [SerializeField]
        string m_ActiveProfileId;
        /// <summary>
        /// The active profile id.
        /// </summary>
        public string activeProfileId
        {
            get
            {
                if (string.IsNullOrEmpty(m_ActiveProfileId))
                    m_ActiveProfileId = m_ProfileSettings.CreateDefaultProfile();
                return m_ActiveProfileId;
            }
            set
            {
                var oldVal = m_ActiveProfileId;
                m_ActiveProfileId = value;

                if (oldVal != value)
                {
                    SetDirty(ModificationEvent.ActiveProfileSet, value, true, true);
                }
            }
        }

        [FormerlySerializedAs("m_hostingServicesManager")]
        [SerializeField]
        HostingServicesManager m_HostingServicesManager;
        /// <summary>
        /// Get the HostingServicesManager object.
        /// </summary>
        public HostingServicesManager HostingServicesManager
        {
            get
            {
                if (m_HostingServicesManager == null)
                    m_HostingServicesManager = new HostingServicesManager();

                if (!m_HostingServicesManager.IsInitialized)
                    m_HostingServicesManager.Initialize(this);

                return m_HostingServicesManager;
            }

            // For unit tests
            internal set { m_HostingServicesManager = value; }
        }

        /// <summary>
        /// Gets all asset entries from all groups.
        /// </summary>
        /// <param name="assets">The list of asset entries.</param>
        /// <param name="includeSubObjects">Determines if sub objects such as sprites should be included.</param>
        /// <param name="groupFilter">A method to filter groups.  Groups will be processed if filter is null, or it returns TRUE</param>
        /// <param name="entryFilter">A method to filter entries.  Entries will be processed if filter is null, or it returns TRUE</param>
        public void GetAllAssets(List<AddressableAssetEntry> assets, bool includeSubObjects, Func<AddressableAssetGroup, bool> groupFilter = null, Func<AddressableAssetEntry, bool> entryFilter = null)
        {
            using (var cache = new AddressablesFileEnumerationCache(this, false, null))
            {
                foreach (var g in groups)
                    if (g != null && (groupFilter == null || groupFilter(g)))
                        g.GatherAllAssets(assets, true, true, includeSubObjects, entryFilter);
            }
        }

        internal void GatherAllAssetReferenceDrawableEntries(List<IReferenceEntryData> assets)
        {
            using (var cache = new AddressablesFileEnumerationCache(this, false, null))
            {
                foreach (var g in groups)
                {
                    if (g != null)
                        g.GatherAllAssetReferenceDrawableEntries(assets);
                }
            }
        }

        /// <summary>
        /// Remove an asset entry.
        /// </summary>
        /// <param name="guid">The  guid of the asset.</param>
        /// <param name="postEvent">Send modifcation event.</param>
        /// <returns>True if the entry was found and removed.</returns>
        public bool RemoveAssetEntry(string guid, bool postEvent = true)
            => RemoveAssetEntry(FindAssetEntry(guid), postEvent);

        /// <summary>
        /// Remove an asset entry.
        /// </summary>
        /// <param name="entry">The entry to remove.</param>
        /// <param name="postEvent">Send modifcation event.</param>
        /// <returns>True if the entry was found and removed.</returns>
        internal bool RemoveAssetEntry(AddressableAssetEntry entry, bool postEvent = true)
        {
            if (entry == null)
                return false;
            if (entry.parentGroup != null)
                entry.parentGroup.RemoveAssetEntry(entry, postEvent);
            return true;
        }

        void Awake()
        {
            profileSettings.OnAfterDeserialize(this);
            buildSettings.OnAfterDeserialize(this);
        }

        void OnEnable()
        {
            HostingServicesManager.OnEnable();
        }

        void OnDisable()
        {
            HostingServicesManager.OnDisable();
        }

        private string m_DefaultGroupTemplateName = "Packed Assets";
        void Validate()
        {
            // Begin update any SchemaTemplate to GroupTemplateObjects
            if (m_SchemaTemplates != null && m_SchemaTemplates.Count > 0)
            {
                Debug.LogError("Updating from GroupSchema version that is too old, deleting schemas");
                m_SchemaTemplates = null;
            }
            if (m_GroupTemplateObjects.Count == 0)
                CreateDefaultGroupTemplate(this);
            // End update of SchemaTemplate to GroupTemplates

            if (m_BuildSettings == null)
                m_BuildSettings = new AddressableAssetBuildSettings();
            if (m_ProfileSettings == null)
                m_ProfileSettings = new AddressableAssetProfileSettings();
            if (m_LabelTable == null)
                m_LabelTable = new LabelTable();
            if (string.IsNullOrEmpty(m_ActiveProfileId))
                m_ActiveProfileId = m_ProfileSettings.CreateDefaultProfile();
            if (m_DataBuilders == null || m_DataBuilders.Count == 0)
            {
                m_DataBuilders = new List<ScriptableObject>();
                m_DataBuilders.Add(CreateScriptAsset<BuildScriptFastMode>());
                m_DataBuilders.Add(CreateScriptAsset<BuildScriptVirtualMode>());
                m_DataBuilders.Add(CreateScriptAsset<BuildScriptPackedPlayMode>());
                m_DataBuilders.Add(CreateScriptAsset<BuildScriptPackedMode>());
            }

            if (ActivePlayerDataBuilder != null && !ActivePlayerDataBuilder.CanBuildData<AddressablesPlayerBuildResult>())
                ActivePlayerDataBuilderIndex = m_DataBuilders.IndexOf(m_DataBuilders.Find(s => s.GetType() == typeof(BuildScriptPackedMode)));
            if (ActivePlayModeDataBuilder != null && !ActivePlayModeDataBuilder.CanBuildData<AddressablesPlayModeBuildResult>())
                ActivePlayModeDataBuilderIndex = m_DataBuilders.IndexOf(m_DataBuilders.Find(s => s.GetType() == typeof(BuildScriptFastMode)));

            profileSettings.Validate(this);
            buildSettings.Validate(this);
        }

        internal T CreateScriptAsset<T>() where T : ScriptableObject
        {
            var script = CreateInstance<T>();
            if (!Directory.Exists(DataBuilderFolder))
                Directory.CreateDirectory(DataBuilderFolder);
            var path = DataBuilderFolder + "/" + typeof(T).Name + ".asset";
            if (!File.Exists(path))
                AssetDatabase.CreateAsset(script, path);
            return AssetDatabase.LoadAssetAtPath<T>(path);
        }

        /// <summary>
        /// The default name of the built in player data AddressableAssetGroup
        /// </summary>
        public const string PlayerDataGroupName = "Built In Data";
        /// <summary>
        /// The default name of the local data AddressableAsssetGroup
        /// </summary>
        public const string DefaultLocalGroupName = "Default Local Group";

        /// <summary>
        /// Create a new AddressableAssetSettings object.
        /// </summary>
        /// <param name="configFolder">The folder to store the settings object.</param>
        /// <param name="configName">The name of the settings object.</param>
        /// <param name="createDefaultGroups">If true, create groups for player data and local packed content.</param>
        /// <param name="isPersisted">If true, assets are created.</param>
        /// <returns></returns>
        public static AddressableAssetSettings Create(string configFolder, string configName, bool createDefaultGroups, bool isPersisted)
        {
            AddressableAssetSettings aa;
            var path = configFolder + "/" + configName + ".asset";
            aa = isPersisted ? AssetDatabase.LoadAssetAtPath<AddressableAssetSettings>(path) : null;
            if (aa == null)
            {
                aa = CreateInstance<AddressableAssetSettings>();
                aa.m_IsTemporary = !isPersisted;
                aa.activeProfileId = aa.profileSettings.Reset();
                aa.name = configName;
                // TODO: Uncomment after initial opt-in testing period
                //aa.ContiguousBundles = true;
                aa.BuildAddressablesWithPlayerBuild = PlayerBuildOption.PreferencesValue;

                if (isPersisted)
                {
                    Directory.CreateDirectory(configFolder);
                    AssetDatabase.CreateAsset(aa, path);
                    aa = AssetDatabase.LoadAssetAtPath<AddressableAssetSettings>(path);
                    aa.Validate();
                }

                if (createDefaultGroups)
                {
                    CreateBuiltInData(aa);
                    CreateDefaultGroup(aa);
                }

                if (isPersisted)
                    AssetDatabase.SaveAssets();
            }
            return aa;
        }

        /// <summary>
        /// Creates a new AddressableAssetGroupTemplate Object with the set of schema types with default settings for use in the editor GUI.
        /// </summary>
        /// <param name="displayName">The display name of the template.</param>
        /// <param name="description">Description text use with the template.</param>
        /// <param name="types">The schema types for the template.</param>
        /// <returns>True if the template was added, false otherwise.</returns>
        public bool CreateAndAddGroupTemplate(string displayName, string description, params Type[] types)
        {
            string assetPath = GroupTemplateFolder + "/" + displayName + ".asset";

            if (!CanCreateGroupTemplate(displayName, assetPath, types))
                return false;

            if (!Directory.Exists(GroupTemplateFolder))
                Directory.CreateDirectory(GroupTemplateFolder);

            AddressableAssetGroupTemplate newAssetGroupTemplate = ScriptableObject.CreateInstance<AddressableAssetGroupTemplate>();
            newAssetGroupTemplate.Description = description;
            newAssetGroupTemplate.Settings = this;

            AssetDatabase.CreateAsset(newAssetGroupTemplate, assetPath);
            AssetDatabase.SaveAssets();

            AddGroupTemplateObject(newAssetGroupTemplate);

            foreach (Type type in types)
                newAssetGroupTemplate.AddSchema(type);


            return true;
        }

        private bool CanCreateGroupTemplate(string displayName, string assetPath, Type[] types)
        {
            if (string.IsNullOrEmpty(displayName))
            {
                Debug.LogWarningFormat("CreateAndAddGroupTemplate - Group template must have a valid name.");
                return false;
            }
            if (types.Length == 0)
            {
                Debug.LogWarningFormat("CreateAndAddGroupTemplate - Group template {0} must contain at least 1 schema type.", displayName);
                return false;
            }
            bool typesAreValid = true;
            for (int i = 0; i < types.Length; i++)
            {
                var t = types[i];
                if (t == null)
                {
                    Debug.LogWarningFormat("CreateAndAddGroupTemplate - Group template {0} schema type at index {1} is null.", displayName, i);
                    typesAreValid = false;
                }
                else if (!typeof(AddressableAssetGroupSchema).IsAssignableFrom(t))
                {
                    Debug.LogWarningFormat("CreateAndAddGroupTemplate - Group template {0} schema type at index {1} must inherit from AddressableAssetGroupSchema.  Specified type was {2}.", displayName, i, t.FullName);
                    typesAreValid = false;
                }
            }
            if (!typesAreValid)
            {
                Debug.LogWarningFormat("CreateAndAddGroupTemplate - Group template {0} must contains at least 1 invalid schema type.", displayName);
                return false;
            }

            if (File.Exists(assetPath))
            {
                Debug.LogWarningFormat("CreateAndAddGroupTemplate - Group template {0} already exists at location {1}.", displayName, assetPath);
                return false;
            }

            return true;
        }

        /// <summary>
        /// Find asset group by functor.
        /// </summary>
        /// <param name="func">The functor to call on each group.  The first group that evaluates to true is returned.</param>
        /// <returns>The group found or null.</returns>
        public AddressableAssetGroup FindGroup(Func<AddressableAssetGroup, bool> func)
        {
            return groups.Find(g => g != null && func(g));
        }

        /// <summary>
        /// Find asset group by name.
        /// </summary>
        /// <param name="groupName">The name of the group.</param>
        /// <returns>The group found or null.</returns>
        public AddressableAssetGroup FindGroup(string groupName)
        {
            return FindGroup(g => g != null && g.Name == groupName);
        }

        /// <summary>
        /// The default group.  This group is used when marking assets as addressable via the inspector.
        /// </summary>
        public AddressableAssetGroup DefaultGroup
        {
            get
            {
                AddressableAssetGroup group = null;
                if (string.IsNullOrEmpty(m_DefaultGroup))
                    group = groups.FirstOrDefault(s => s != null && s.CanBeSetAsDefault());
                else
                {
                    group = groups.FirstOrDefault(x => x != null && x.Guid == m_DefaultGroup);
                    if (group == null || !group.CanBeSetAsDefault())
                    {
                        group = groups.FirstOrDefault(s => s != null && s.CanBeSetAsDefault());
                        if (group != null)
                            m_DefaultGroup = group.Guid;
                    }
                }

                if (group == null)
                {
                    Addressables.LogWarning("A valid default group could not be found.  One will be created.");
                    group = CreateDefaultGroup(this);
                }

                return group;
            }
            set
            {
                if (value == null)
                    Addressables.LogError("Unable to set null as the Default Group.  Default Groups must not be ReadOnly.");

                else if (!value.CanBeSetAsDefault())
                    Addressables.LogError("Unable to set " + value.Name + " as the Default Group.  Default Groups must not be ReadOnly.");
                else
                    m_DefaultGroup = value.Guid;
            }
        }

        internal static AddressableAssetGroup CreateBuiltInData(AddressableAssetSettings aa)
        {
            var playerData = aa.CreateGroup(PlayerDataGroupName, false, true, false, null, typeof(PlayerDataGroupSchema));
            var resourceEntry = aa.CreateOrMoveEntry(AddressableAssetEntry.ResourcesName, playerData, false, false);
            resourceEntry.IsInResources = true;
            aa.CreateOrMoveEntry(AddressableAssetEntry.EditorSceneListName, playerData, false, false);
            return playerData;
        }

        private static AddressableAssetGroup CreateDefaultGroup(AddressableAssetSettings aa)
        {
            var localGroup = aa.CreateGroup(DefaultLocalGroupName, true, false, false, null, typeof(ContentUpdateGroupSchema), typeof(BundledAssetGroupSchema));
            var schema = localGroup.GetSchema<BundledAssetGroupSchema>();
            schema.BuildPath.SetVariableByName(aa, kLocalBuildPath);
            schema.LoadPath.SetVariableByName(aa, kLocalLoadPath);
            schema.BundleMode = BundledAssetGroupSchema.BundlePackingMode.PackTogether;
            aa.m_DefaultGroup = localGroup.Guid;
            return localGroup;
        }

        private static bool CreateDefaultGroupTemplate(AddressableAssetSettings aa)
        {
            string assetPath = aa.GroupTemplateFolder + "/" + aa.m_DefaultGroupTemplateName + ".asset";

            if (File.Exists(assetPath))
                return LoadGroupTemplateObject(aa, assetPath);

            return aa.CreateAndAddGroupTemplate(aa.m_DefaultGroupTemplateName, "Pack assets into asset bundles.", typeof(BundledAssetGroupSchema), typeof(ContentUpdateGroupSchema));
        }

        private static bool LoadGroupTemplateObject(AddressableAssetSettings aa, string assetPath)
        {
            return aa.AddGroupTemplateObject(AssetDatabase.LoadAssetAtPath(assetPath, typeof(ScriptableObject)) as IGroupTemplate);
        }

        internal AddressableAssetEntry CreateEntry(string guid, string address, AddressableAssetGroup parent, bool readOnly, bool postEvent = true)
        {
            AddressableAssetEntry entry = parent.GetAssetEntry(guid);
            if (entry == null)
                entry = new AddressableAssetEntry(guid, address, parent, readOnly);

            if (!readOnly)
                SetDirty(ModificationEvent.EntryCreated, entry, postEvent, false);

            return entry;
        }

        /// <summary>
        /// Marks the object as modified.
        /// </summary>
        /// <param name="modificationEvent">The event type that is changed.</param>
        /// <param name="eventData">The object data that corresponds to the event.</param>
        /// <param name="postEvent">If true, the event is propagated to callbacks.</param>
        /// <param name="settingsModified">If true, the settings asset will be marked as dirty.</param>
        public void SetDirty(ModificationEvent modificationEvent, object eventData, bool postEvent, bool settingsModified = false)
        {
            if (modificationEvent == ModificationEvent.ProfileRemoved && eventData as string == activeProfileId)
                activeProfileId = null;
            if (this != null)
            {
                if (postEvent)
                {
                    if (OnModificationGlobal != null)
                        OnModificationGlobal(this, modificationEvent, eventData);
                    if (OnModification != null)
                        OnModification(this, modificationEvent, eventData);
                }

                if (settingsModified && IsPersisted)
                    EditorUtility.SetDirty(this);
            }

            m_CachedHash = default(Hash128);
        }

        internal bool RemoveMissingGroupReferences()
        {
            List<int> missingGroupsIndices = new List<int>();
            for (int i = 0; i < groups.Count; i++)
            {
                var g = groups[i];
                if (g == null)
                    missingGroupsIndices.Add(i);
            }
            if (missingGroupsIndices.Count > 0)
            {
                Debug.Log("Addressable settings contains " + missingGroupsIndices.Count + " group reference(s) that are no longer there. Removing reference(s).");
                for (int i = missingGroupsIndices.Count - 1; i >= 0; i--)
                {
                    groups.RemoveAt(missingGroupsIndices[i]);
                }
                return true;
            }
            return false;
        }

        /// <summary>
        /// Find and asset entry by guid.
        /// </summary>
        /// <param name="guid">The asset guid.</param>
        /// <returns>The found entry or null.</returns>
        public AddressableAssetEntry FindAssetEntry(string guid)
        {
            return FindAssetEntry(guid, false);
        }

        /// <summary>
        /// Find and asset entry by guid.
        /// </summary>
        /// <param name="guid">The asset guid.</param>
        /// <param name="includeImplicit">Whether or not to include implicit asset entries in the search.</param>
        /// <returns>The found entry or null.</returns>
        public AddressableAssetEntry FindAssetEntry(string guid, bool includeImplicit)
        {
            AddressableAssetEntry foundEntry = null;
            if (m_FindAssetEntryCache != null)
            {
                if (m_FindAssetEntryCache.TryGetCached(guid, out foundEntry))
                    return foundEntry;
            }
            else
                m_FindAssetEntryCache = new Cache<string, AddressableAssetEntry>(this);
            
            if (includeImplicit)
            {
                for (int i = 0; i < groups.Count; ++i)
                {
                    if (groups[i] == null)
                        continue;

                    if (groups[i].EntryMap.TryGetValue(guid, out foundEntry))
                    {
                        m_FindAssetEntryCache.Add(guid, foundEntry);
                        return foundEntry;
                    }
        
                    if (groups[i].AssetCollectionEntries.Count > 0)
                    {
                        foreach (AddressableAssetEntry addressableAssetEntry in groups[i].AssetCollectionEntries)
                        {
                            foundEntry = addressableAssetEntry.GetAssetCollectionSubEntry(guid);
                            if (foundEntry != null)
                            {
                                m_FindAssetEntryCache.Add(guid, foundEntry);
                                return foundEntry;
                            }
                        }
                    }
                }
                
                string path = AssetDatabase.GUIDToAssetPath(guid);
                if (!AddressableAssetUtility.IsPathValidForEntry(path))
                    return null;
        
                // find an explicit parent folder entry within groups
                string directory = Path.GetDirectoryName(path);
                while (!string.IsNullOrEmpty(directory))
                {
                    string folderGuid = AssetDatabase.AssetPathToGUID(directory);
                    for (int i = 0; i < groups.Count; ++i)
                    {
                        if (groups[i] == null)
                            continue;
                        if (groups[i].EntryMap.TryGetValue(folderGuid, out foundEntry))
                        {
                            foundEntry = foundEntry.GetFolderSubEntry(guid, path);
                            if (foundEntry != null)
                            {
                                m_FindAssetEntryCache.Add(guid, foundEntry);
                                return foundEntry;
                            }
                            Debug.LogError($"Explicit AssetEntry for {directory} unable to find subEntry {path}");
                            return null;
                        }
                    }
                    directory = Path.GetDirectoryName(directory);
                }
                m_FindAssetEntryCache.Add(guid, null);
            }
            else
            {
                for (int i = 0; i < groups.Count; ++i)
                {
                    if (groups[i] == null)
                        continue;
                    foundEntry = groups[i].GetAssetEntry(guid);
                    if (foundEntry != null)
                    {
                        m_FindAssetEntryCache.Add(guid, foundEntry);
                        return foundEntry;
                    }
                }
            }
            
            return null;
        }

        internal bool IsAssetPathInAddressableDirectory(string assetPath, out string assetName)
        {
            if (!string.IsNullOrEmpty(assetPath))
            {
                var dir = Path.GetDirectoryName(assetPath);
                while (!string.IsNullOrEmpty(dir))
                {
                    var dirEntry = FindAssetEntry(AssetDatabase.AssetPathToGUID(dir));
                    if (dirEntry != null)
                    {
                        assetName = dirEntry.address + assetPath.Remove(0, dir.Length);
                        return true;
                    }

                    dir = Path.GetDirectoryName(dir);
                }
            }
            assetName = "";
            return false;
        }

        internal void MoveAssetsFromResources(Dictionary<string, string> guidToNewPath, AddressableAssetGroup targetParent)
        {
            if (guidToNewPath == null || targetParent == null)
            {
                return;
            }

            var entries = new List<AddressableAssetEntry>();
            var createdDirs = new List<string>();
            AssetDatabase.StartAssetEditing();
            foreach (var item in guidToNewPath)
            {
                var dirInfo = new FileInfo(item.Value).Directory;
                if (dirInfo != null && !dirInfo.Exists)
                {
                    dirInfo.Create();
                    createdDirs.Add(dirInfo.FullName);
                    AssetDatabase.StopAssetEditing();
                    AssetDatabase.Refresh();
                    AssetDatabase.StartAssetEditing();
                }

                var oldPath = AssetDatabase.GUIDToAssetPath(item.Key);
                var errorStr = AssetDatabase.MoveAsset(oldPath, item.Value);
                if (!string.IsNullOrEmpty(errorStr))
                {
                    Addressables.LogError("Error moving asset: " + errorStr);
                }
                else
                {
                    AddressableAssetEntry e = FindAssetEntry(item.Key);
                    if (e != null)
                        e.IsInResources = false;

                    var newEntry = CreateOrMoveEntry(item.Key, targetParent, false, false);
                    var index = oldPath.ToLower().LastIndexOf("resources/");
                    if (index >= 0)
                    {
                        var newAddress = oldPath.Substring(index + 10);
                        if (Path.HasExtension(newAddress))
                        {
                            newAddress = newAddress.Replace(Path.GetExtension(oldPath), "");
                        }

                        if (!string.IsNullOrEmpty(newAddress))
                        {
                            newEntry.SetAddress(newAddress, false);
                        }
                    }
                    entries.Add(newEntry);
                }
            }

            foreach (var dir in createdDirs)
                DirectoryUtility.DeleteDirectory(dir, onlyIfEmpty: true);

            AssetDatabase.StopAssetEditing();
            AssetDatabase.Refresh();
            SetDirty(ModificationEvent.EntryMoved, entries, true, true);
        }

        /// <summary>
        /// Move an existing entry to a group.
        /// </summary>
        /// <param name="entries">The entries to move.</param>
        /// <param name="targetParent">The group to add the entries to.</param>
        /// <param name="readOnly">Should the entries be read only.</param>
        /// <param name="postEvent">Send modification event.</param>
        public void MoveEntries(List<AddressableAssetEntry> entries, AddressableAssetGroup targetParent, bool readOnly = false, bool postEvent = true)
        {
            if (entries != null)
            {
                foreach (var entry in entries)
                {
                    MoveEntry(entry, targetParent, readOnly, false);
                }

                SetDirty(ModificationEvent.EntryMoved, entries, postEvent, false);
            }
        }

        /// <summary>
        /// Move an existing entry to a group.
        /// </summary>
        /// <param name="entry">The entry to move.</param>
        /// <param name="targetParent">The group to add the entry to.</param>
        /// <param name="readOnly">Should the entry be read only.</param>
        /// <param name="postEvent">Send modification event.</param>
        public void MoveEntry(AddressableAssetEntry entry, AddressableAssetGroup targetParent, bool readOnly = false, bool postEvent = true)
        {
            if (targetParent == null || entry == null)
                return;

            entry.ReadOnly = readOnly;

            if (entry.parentGroup != null && entry.parentGroup != targetParent)
                entry.parentGroup.RemoveAssetEntry(entry, postEvent);

            targetParent.AddAssetEntry(entry, postEvent);
        }

        /// <summary>
        /// Create a new entry, or if one exists in a different group, move it into the new group.
        /// </summary>
        /// <param name="guid">The asset guid.</param>
        /// <param name="targetParent">The group to add the entry to.</param>
        /// <param name="readOnly">Is the new entry read only.</param>
        /// <param name="postEvent">Send modification event.</param>
        /// <returns></returns>
        public AddressableAssetEntry CreateOrMoveEntry(string guid, AddressableAssetGroup targetParent, bool readOnly = false, bool postEvent = true)
        {
            if (targetParent == null || string.IsNullOrEmpty(guid))
                return null;

            AddressableAssetEntry entry = FindAssetEntry(guid);
            if (entry != null) //move entry to where it should go...
            {
                MoveEntry(entry, targetParent, readOnly, postEvent);
            }
            else //create entry
            {
                entry = CreateAndAddEntryToGroup(guid, targetParent, readOnly, postEvent);
            }

            return entry;
        }

        /// <summary>
        /// Create a new entries for each asset, or if one exists in a different group, move it into the targetParent group.
        /// </summary>
        /// <param name="guids">The asset guid's to move.</param>
        /// <param name="targetParent">The group to add the entries to.</param>
        /// <param name="createdEntries">List to add new entries to.</param>
        /// <param name="movedEntries">List to add moved entries to.</param>
        /// <param name="readOnly">Is the new entry read only.</param>
        /// <param name="postEvent">Send modification event.</param>
        /// <exception cref="ArgumentException"></exception>
        internal void CreateOrMoveEntries(IEnumerable guids, AddressableAssetGroup targetParent, List<AddressableAssetEntry> createdEntries, List<AddressableAssetEntry> movedEntries, bool readOnly = false, bool postEvent = true)
        {
            if (targetParent == null)
                throw new ArgumentException("targetParent must not be null");

            if (createdEntries == null)
                createdEntries = new List<AddressableAssetEntry>();
            if (movedEntries == null)
                movedEntries = new List<AddressableAssetEntry>();

            foreach (string guid in guids)
            {
                AddressableAssetEntry entry = FindAssetEntry(guid);
                if (entry != null)
                {
                    MoveEntry(entry, targetParent, readOnly, postEvent);
                    movedEntries.Add(entry);
                }
                else
                {
                    entry = CreateAndAddEntryToGroup(guid, targetParent, readOnly, postEvent);
                    if (entry != null)
                        createdEntries.Add(entry);
                }
            }
        }

        private AddressableAssetEntry CreateAndAddEntryToGroup(string guid, AddressableAssetGroup targetParent, bool readOnly = false, bool postEvent = true)
        {
            AddressableAssetEntry entry = null;
            var path = AssetDatabase.GUIDToAssetPath(guid);

            if (AddressableAssetUtility.IsPathValidForEntry(path))
            {
                entry = CreateEntry(guid, path, targetParent, readOnly, postEvent);
            }
            else
            {
                if (AssetDatabase.GetMainAssetTypeAtPath(path) != null && BuildUtility.IsEditorAssembly(AssetDatabase.GetMainAssetTypeAtPath(path).Assembly))
                    return null;
                entry = CreateEntry(guid, guid, targetParent, true, postEvent);
            }

            targetParent.AddAssetEntry(entry, postEvent);
            return entry;
        }

        internal AddressableAssetEntry CreateSubEntryIfUnique(string guid, string address, AddressableAssetEntry parentEntry)
        {
            if (string.IsNullOrEmpty(guid))
                return null;

            AddressableAssetEntry entry = FindAssetEntry(guid);

            if (entry == null)
            {
                entry = new AddressableAssetEntry(guid, address, parentEntry.parentGroup, true);
                entry.IsSubAsset = true;
                entry.ParentEntry = parentEntry;
                entry.BundleFileId = parentEntry.BundleFileId;
                //parentEntry.parentGroup.AddAssetEntry(entry);
                return entry;
            }

            //if the sub-entry already exists update it's info.  This mainly covers the case of dragging folders around.
            if (entry.IsSubAsset)
            {
                entry.parentGroup = parentEntry.parentGroup;
                entry.IsInResources = parentEntry.IsInResources;
                entry.address = address;
                entry.ReadOnly = true;
                entry.BundleFileId = parentEntry.BundleFileId;
                return entry;
            }
            return null;
        }

        /// <summary>
        /// Create a new asset group.
        /// </summary>
        /// <param name="groupName">The group name.</param>
        /// <param name="setAsDefaultGroup">Set the new group as the default group.</param>
        /// <param name="readOnly">Is the new group read only.</param>
        /// <param name="postEvent">Post modification event.</param>
        /// <param name="schemasToCopy">Schema set to copy from.</param>
        /// <param name="types">Types of schemas to add.</param>
        /// <returns>The newly created group.</returns>
        public AddressableAssetGroup CreateGroup(string groupName, bool setAsDefaultGroup, bool readOnly, bool postEvent, List<AddressableAssetGroupSchema> schemasToCopy, params Type[] types)
        {
            if (string.IsNullOrEmpty(groupName))
                groupName = kNewGroupName;
            string validName = FindUniqueGroupName(groupName);
            var group = CreateInstance<AddressableAssetGroup>();
            group.Initialize(this, validName, GUID.Generate().ToString(), readOnly);

            if (IsPersisted)
            {
                if (!Directory.Exists(GroupFolder))
                    Directory.CreateDirectory(GroupFolder);
                AssetDatabase.CreateAsset(group, GroupFolder + "/" + group.Name + ".asset");
            }
            if (schemasToCopy != null)
            {
                foreach (var s in schemasToCopy)
                    group.AddSchema(s, false);
            }
            foreach (var t in types)
                group.AddSchema(t);

            if (!m_GroupAssets.Contains(group))
                groups.Add(group);


+            // yukimoto added.
+            groups.Sort((a, b) => string.Compare(a.Name, b.Name));

            if (setAsDefaultGroup)
                DefaultGroup = group;
            SetDirty(ModificationEvent.GroupAdded, group, postEvent, true);
            AddressableAssetUtility.OpenAssetIfUsingVCIntegration(this);
            return group;
        }

        internal string FindUniqueGroupName(string potentialName)
        {
            var cleanedName = potentialName.Replace('/', '-');
            cleanedName = cleanedName.Replace('\\', '-');
            if (cleanedName != potentialName)
                Addressables.Log("Group names cannot include '\\' or '/'.  Replacing with '-'. " + cleanedName);
            var validName = cleanedName;
            int index = 1;
            bool foundExisting = true;
            while (foundExisting)
            {
                if (index > 1000)
                {
                    Addressables.LogError("Unable to create valid name for new Addressable Assets group.");
                    return cleanedName;
                }
                foundExisting = IsNotUniqueGroupName(validName);
                if (foundExisting)
                {
                    validName = cleanedName + index;
                    index++;
                }
            }

            return validName;
        }

        internal bool IsNotUniqueGroupName(string groupName)
        {
            bool foundExisting = false;
            foreach (var g in groups)
            {
                if (g != null && g.Name == groupName)
                {
                    foundExisting = true;
                    break;
                }
            }
            return foundExisting;
        }

        /// <summary>
        /// Remove an asset group.
        /// </summary>
        /// <param name="g"></param>
        public void RemoveGroup(AddressableAssetGroup g)
        {
            AssetDatabase.StartAssetEditing();
            try
            {
                RemoveGroupInternal(g, true, true);
            }
            finally
            {
                AssetDatabase.StopAssetEditing();
            }
        }

        internal void RemoveGroupInternal(AddressableAssetGroup g, bool deleteAsset, bool postEvent)
        {
            g?.ClearSchemas(true);
            groups.Remove(g);
            SetDirty(ModificationEvent.GroupRemoved, g, postEvent, true);
            if (g != null && deleteAsset)
            {
                string guidOfGroup;
                long localId;
                if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(g, out guidOfGroup, out localId))
                {
                    var groupPath = AssetDatabase.GUIDToAssetPath(guidOfGroup);
                    if (!string.IsNullOrEmpty(groupPath))
                        AssetDatabase.DeleteAsset(groupPath);
                }
            }
        }

        internal void SetLabelValueForEntries(List<AddressableAssetEntry> entries, string label, bool value, bool postEvent = true)
        {
            var addedNewLabel = value && m_LabelTable.AddLabelName(label);

            foreach (var e in entries)
            {
                e.SetLabel(label, value, false, false);
                AddressableAssetUtility.OpenAssetIfUsingVCIntegration(e.parentGroup);
            }

            SetDirty(ModificationEvent.EntryModified, entries, postEvent, addedNewLabel);
            AddressableAssetUtility.OpenAssetIfUsingVCIntegration(this);
        }

        internal void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
        {
            List<string> assetEntryCollections = new List<string>();
            var aa = this;
            bool relatedAssetChanged = false;
            bool settingsChanged = false;
            foreach (string str in importedAssets)
            {
                var assetType = AssetDatabase.GetMainAssetTypeAtPath(str);
                if (typeof(AddressableAssetSettings).IsAssignableFrom(assetType))
                {
                    var settings = AssetDatabase.LoadAssetAtPath<AddressableAssetSettings>(str);
                    if (settings != null)
                        settings.Validate();
                }
                if (typeof(AddressableAssetGroup).IsAssignableFrom(assetType))
                {
                    AddressableAssetGroup group = aa.FindGroup(Path.GetFileNameWithoutExtension(str));
                    if (group == null)
                    {
                        var foundGroup = AssetDatabase.LoadAssetAtPath<AddressableAssetGroup>(str);
                        if (!aa.groups.Contains(foundGroup))
                        {
                            aa.groups.Add(foundGroup);
                            group = aa.FindGroup(Path.GetFileNameWithoutExtension(str));
                            relatedAssetChanged = true;
                            settingsChanged = true;
                        }
                    }
                    if (group != null)
                        group.DedupeEnteries();
                }

#pragma warning disable 0618
                if (typeof(AddressableAssetEntryCollection).IsAssignableFrom(assetType))
                    assetEntryCollections.Add(str);
#pragma warning restore 0618

                var guid = AssetDatabase.AssetPathToGUID(str);
                if (aa.FindAssetEntry(guid) != null)
                    relatedAssetChanged = true;

                if (AddressableAssetUtility.IsInResources(str))
                    relatedAssetChanged = true;
            }

            if (assetEntryCollections.Count > 0)
                relatedAssetChanged = ConvertAssetEntryCollectionsWithPermissionRequest(assetEntryCollections) || relatedAssetChanged;

            if (deletedAssets.Length > 0)
            {
                // if any directly referenced assets were deleted while Unity was closed, the path isn't useful, so Remove(null) is our only option
                //  this can lead to orphaned schema files.
                if (groups.Remove(null) ||
                    DataBuilders.Remove(null) ||
                    GroupTemplateObjects.Remove(null) ||
                    InitializationObjects.Remove(null))
                {
                    relatedAssetChanged = true;
                }
            }

            foreach (string str in deletedAssets)
            {
                if (AddressableAssetUtility.IsInResources(str))
                    relatedAssetChanged = true;
                else
                {
                    if (CheckForGroupDataDeletion(str))
                    {
                        relatedAssetChanged = true;
                        settingsChanged = true;
                        continue;
                    }

                    var guidOfDeletedAsset = AssetDatabase.AssetPathToGUID(str);
                    if (aa.RemoveAssetEntry(guidOfDeletedAsset))
                    {
                        relatedAssetChanged = true;
                    }
                }
            }
            for (int i = 0; i < movedAssets.Length; i++)
            {
                var str = movedAssets[i];
                var assetType = AssetDatabase.GetMainAssetTypeAtPath(str);
                if (typeof(AddressableAssetGroup).IsAssignableFrom(assetType))
                {
                    var oldGroupName = Path.GetFileNameWithoutExtension(movedFromAssetPaths[i]);
                    var group = aa.FindGroup(oldGroupName);
                    if (group != null)
                    {
                        var newGroupName = Path.GetFileNameWithoutExtension(str);
                        group.Name = newGroupName;
                        relatedAssetChanged = true;
                    }
                }
                else
                {
                    var guid = AssetDatabase.AssetPathToGUID(str);
                    AddressableAssetEntry entry = aa.FindAssetEntry(guid);

                    bool isAlreadyAddressable = entry != null;
                    bool startedInResources = AddressableAssetUtility.IsInResources(movedFromAssetPaths[i]);
                    bool endedInResources = AddressableAssetUtility.IsInResources(str);
                    bool inEditorSceneList = BuiltinSceneCache.Contains(new GUID(guid));

                    //update entry cached path
                    entry?.SetCachedPath(str);

                    //move to Resources
                    if (isAlreadyAddressable && endedInResources)
                    {
                        var fileName = Path.GetFileNameWithoutExtension(str);
                        Addressables.Log("You have moved addressable asset " + fileName + " into a Resources directory.  It has been unmarked as addressable, but can still be loaded via the Addressables API via its Resources path.");
                        aa.RemoveAssetEntry(guid, false);
                    }
                    else if (inEditorSceneList)
                        BuiltinSceneCache.ClearState();

                    //any addressables move or resources move (even resources to within resources) needs to refresh the UI.
                    relatedAssetChanged = isAlreadyAddressable || startedInResources || endedInResources || inEditorSceneList;
                }
            }

            if (relatedAssetChanged || settingsChanged)
                aa.SetDirty(ModificationEvent.BatchModification, null, true, settingsChanged);
        }

#pragma warning disable 0618
        internal bool ConvertAssetEntryCollectionsWithPermissionRequest(List<string> assetEntryCollections)
        {
            if (assetEntryCollections == null || assetEntryCollections.Count == 0 || DenyEntryCollectionPermission)
                return false;

            bool allowConvertCollectionToEntries = EditorUtility.GetDialogOptOutDecision(DialogOptOutDecisionType.ForThisMachine, kImportAssetEntryCollectionOptOutKey);
            if (!allowConvertCollectionToEntries)
            {
                allowConvertCollectionToEntries = EditorUtility.DisplayDialog("AssetEntryCollection Found",
                    "AssetEntryCollection is obsolete, do you want create AddressableAssetEntries from the AssetEntryCollection in the Default Group and remove the AssetEntryCollection from the project?",
                    "Yes", "No",
                    DialogOptOutDecisionType.ForThisMachine, kImportAssetEntryCollectionOptOutKey);
            }
            return allowConvertCollectionToEntries ? ConvertAssetEntryCollections(assetEntryCollections) : false;
        }

        internal bool ConvertAssetEntryCollections(List<string> assetEntryCollections)
        {
            if (assetEntryCollections == null || assetEntryCollections.Count == 0)
                return false;

            bool changesMade = false;
            foreach (string collectionPath in assetEntryCollections)
            {
                var collection = AssetDatabase.LoadAssetAtPath<AddressableAssetEntryCollection>(collectionPath);
                if (collection == null)
                {
                    Debug.LogError("Could not load and convert AssetEntryCollection at " + collectionPath);
                    continue;
                }
                if (!AddressableAssetEntryCollection.ConvertEntryCollectionToEntries(collection, this))
                {
                    Debug.LogError("Failed to convert AssetEntryCollection to AddressableAssetEntries at " + collectionPath);
                    continue;
                }

                if (collectionPath.StartsWith("Assets"))
                {
                    if (!AssetDatabase.DeleteAsset(collectionPath))
                        Debug.LogError("Failed to Delete AssetEntryCollection at " + collectionPath);
                }
                else
                {
                    Debug.LogWarning($"Imported AssetEntryCollection is in a Package, deletion of Asset at {collectionPath} aborted.");
                }
                changesMade = true;
            }
            return changesMade;
        }
#pragma warning restore 0618

        internal bool CheckForGroupDataDeletion(string str)
        {
            if (string.IsNullOrEmpty(str))
                return false;

            bool modified = false;
            AddressableAssetGroup groupToDelete = null;
            bool deleteGroup = false;
            foreach (var group in groups)
            {
                if (group != null)
                {
                    if (AssetDatabase.GUIDToAssetPath(group.Guid) == str)
                    {
                        groupToDelete = group;
                        deleteGroup = true;
                        break;
                    }

                    if (group.Schemas.Remove(null))
                        modified = true;
                }
            }

            if (deleteGroup)
            {
                RemoveGroupInternal(groupToDelete, false, true);
                modified = true;
            }

            return modified;
        }

        /// <summary>
        /// Runs the active player data build script to create runtime data.
        /// See the [BuildPlayerContent](xref:addressables-api-build-player-content) documentation for more details.
        /// </summary>
        public static void BuildPlayerContent()
        {
            BuildPlayerContent(out AddressablesPlayerBuildResult rst);
        }

#if (ENABLE_CCD && UNITY_2019_4_OR_NEWER)
        /// <summary>
        /// Runs the active player data build script to create runtime data.
        /// Any groups referencing CCD group type will have the produced bundles uploaded to the specified non-promotion only bucket.
        /// See the [BuildPlayerContent](xref:addressables-api-build-player-content) documentation for more details.
        /// </summary>
        public async static Task<AddressableAssetBuildResult> BuildAndReleasePlayerContent()
        {
            AddressableAssetBuildResult result = null;
            var settings = AddressableAssetSettingsDefaultObject.Settings;
            if (settings == null)
            {
                string error;
                if (EditorApplication.isUpdating)
                    error = "Addressable Asset Settings does not exist.  EditorApplication.isUpdating was true.";
                else if (EditorApplication.isCompiling)
                    error = "Addressable Asset Settings does not exist.  EditorApplication.isCompiling was true.";
                else
                    error = "Addressable Asset Settings does not exist.  Failed to create.";
                Addressables.LogError(error);
                result = new AddressablesPlayerBuildResult();
                result.Error = error;
                return result;
            }

            NullifyBundleFileIds(settings);

            //Processing groups, checking for promotion buckets
            bool promotionOnly = GroupsContainPromotionOnlyBucket(settings);
            if (promotionOnly)
            {
                result = new AddressablesPlayerBuildResult();
                result.Error = "Cannot upload to Promotion Only bucket.";
                return result;
            }

            //Reclean directory before every build
            if (Directory.Exists(kCCDBuildDataPath))
            {
                Directory.Delete(kCCDBuildDataPath, true);
            }

            //Build the player content
            result = settings.BuildPlayerContentImpl(new AddressablesDataBuilderInput(settings), true);

            //Getting files
            Addressables.Log("Creating and uploading entries");
            var startDirectory = new DirectoryInfo(kCCDBuildDataPath);
            var buckets = CreateBucketData(startDirectory);


            //Creating a release for each bucket
            await CreateReleaseForBuckets(buckets);

            return result;

        }

        static Dictionary<DirectoryInfo, Dictionary<DirectoryInfo, List<FileInfo>>> CreateBucketData(DirectoryInfo startDirectory)
        {
            var buckets = new Dictionary<DirectoryInfo, Dictionary<DirectoryInfo, List<FileInfo>>>();
            var bucketDirs = startDirectory.GetDirectories().Where(d => !d.Attributes.HasFlag(FileAttributes.Hidden));
            foreach (var bucketDir in bucketDirs)
            {
                var badgeDirs = bucketDir.GetDirectories().Where(d => !d.Attributes.HasFlag(FileAttributes.Hidden));
                foreach (var badgeDir in badgeDirs)
                {
                    var files = badgeDir.GetFiles().Where(f => !f.Attributes.HasFlag(FileAttributes.Hidden)).ToList();
                    if (!buckets.ContainsKey(bucketDir))
                    {
                        var badges = new Dictionary<DirectoryInfo, List<FileInfo>>();
                        badges.Add(badgeDir, files);
                        buckets.Add(bucketDir, badges);
                    }
                    else
                    {
                        buckets.TryGetValue(bucketDir, out var badges);
                        if (!badges.ContainsKey(badgeDir))
                        {
                            badges.Add(badgeDir, files);
                        }
                        else
                        {
                            badges.TryGetValue(badgeDir, out var existingFiles);
                            existingFiles.AddRange(files);
                        }
                    }
                }
            }

            return buckets;
        }

        async static Task CreateReleaseForBuckets(Dictionary<DirectoryInfo, Dictionary<DirectoryInfo, List<FileInfo>>> buckets)
        {
            foreach (var bucketKvp in buckets)
            {
                Guid bucketId = Guid.Parse(bucketKvp.Key.Name);

                foreach (var badgeKvp in bucketKvp.Value)
                {
                    string badgeName = badgeKvp.Key.Name;
                    List<CcdReleaseEntryCreate> entries = new List<CcdReleaseEntryCreate>();

                    foreach (var path in badgeKvp.Value)
                    {
                        string contentHash = AddressableAssetUtility.GetMd5Hash(path.FullName);
                        using (var stream = File.OpenRead(path.FullName))
                        {
                            var entryPath = path.Name;
                            var entryModelOptions = new EntryModelOptions(entryPath, contentHash, (int)stream.Length)
                            {
                                UpdateIfExists = true
                            };
                            var createdEntry = await CcdManagement.Instance.CreateOrUpdateEntryByPathAsync(new EntryByPathOptions(bucketId, entryPath), entryModelOptions);

                            var uploadContentOptions = new UploadContentOptions(bucketId, createdEntry.Entryid, stream);
                            await CcdManagement.Instance.UploadContentAsync(uploadContentOptions);

                            entries.Add(new CcdReleaseEntryCreate(createdEntry.Entryid, createdEntry.CurrentVersionid));
                        }
                    }

                    //Creating release
                    Addressables.Log("Creating release.");
                    var release = await CcdManagement.Instance.CreateReleaseAsync(new CreateReleaseOptions(bucketId)
                    {
                        Entries = entries,
                        Notes = $"Automated release created for {badgeName}"
                    });
                    Addressables.Log($"Release {release.Releaseid} created.");

                    //Don't update latest badge (as it always updates)
                    if (badgeName != "latest")
                    {
                        //Updating badge
                        Addressables.Log("Updating badge.");
                        var badge = await CcdManagement.Instance.AssignBadgeAsync(new AssignBadgeOptions(bucketId, badgeName, release.Releaseid));
                        Addressables.Log($"Badge {badge.Name} updated.");
                    }
                }
            }
        }

        /// <summary>
        /// Check if groups contain promotion only buckets.
        /// </summary>
        /// <param name="settings">The Settings to process</param>
        /// <returns>True if any group points to a promotion only bucket.</returns>
        internal static bool GroupsContainPromotionOnlyBucket(AddressableAssetSettings settings)
        {
            foreach (AddressableAssetGroup group in settings.groups)
            {
                if (group == null)
                    continue;

                var schema = group.GetSchema<BundledAssetGroupSchema>();
                if (schema != null)
                {
                    var buildPath = schema.BuildPath.GetValue(settings);
                    var loadPath = schema.LoadPath.GetValue(settings);
                    var groupType = new ProfileGroupType("temp");
                    groupType.AddVariable(new ProfileGroupType.GroupTypeVariable(kBuildPath, buildPath));
                    groupType.AddVariable(new ProfileGroupType.GroupTypeVariable(kLoadPath, loadPath));
                    var foundGroupType = ProfileDataSourceSettings.GetSettings().FindGroupType(groupType);
                    if (foundGroupType != null && foundGroupType.GroupTypePrefix.StartsWith("CCD"))
                    {
                        if (bool.Parse(foundGroupType.GetVariableBySuffix(nameof(CcdBucket.Attributes.PromoteOnly)).Value) == true)
                        {
                            string error = "Cannot upload to Promotion Only bucket.";
                            Addressables.LogError(error);
                            return true;
                        }
                    }

                }
            }

            return false;
        }
#endif

        /// <summary>
        /// Runs the active player data build script to create runtime data.
        /// See the [BuildPlayerContent](xref:addressables-api-build-player-content) documentation for more details.
        /// </summary>
        /// <param name="result">Results from running the active player data build script.</param>
        public static void BuildPlayerContent(out AddressablesPlayerBuildResult result)
        {
            BuildPlayerContent(out result, null);
        }
        
        internal static void BuildPlayerContent(out AddressablesPlayerBuildResult result, AddressablesDataBuilderInput input)
        {
            var settings = input != null ? input.AddressableSettings : AddressableAssetSettingsDefaultObject.Settings;
            if (settings == null)
            {
                string error;
                if (EditorApplication.isUpdating)
                    error = "Addressable Asset Settings does not exist.  EditorApplication.isUpdating was true.";
                else if (EditorApplication.isCompiling)
                    error = "Addressable Asset Settings does not exist.  EditorApplication.isCompiling was true.";
                else
                    error = "Addressable Asset Settings does not exist.  Failed to create.";
                Debug.LogError(error);
                result = new AddressablesPlayerBuildResult();
                result.Error = error;
                return;
            }

            NullifyBundleFileIds(settings);

            result = settings.BuildPlayerContentImpl(input);
        }

        internal static void NullifyBundleFileIds(AddressableAssetSettings settings)
        {
            foreach (AddressableAssetGroup group in settings.groups)
            {
                if (group == null)
                    continue;
                foreach (AddressableAssetEntry entry in group.entries)
                    entry.BundleFileId = null;
            }
        }

        internal AddressablesPlayerBuildResult BuildPlayerContentImpl(AddressablesDataBuilderInput buildContext = null, bool buildAndRelease = false)
        {
            if (Directory.Exists(Addressables.BuildPath))
            {
                try
                {
                    Directory.Delete(Addressables.BuildPath, true);
                }
                catch (Exception e)
                {
                    Debug.LogException(e);
                }
            }

            if (buildContext == null)
                buildContext = new AddressablesDataBuilderInput(this);
                
            buildContext.IsBuildAndRelease = buildAndRelease;
            var result = ActivePlayerDataBuilder.BuildData<AddressablesPlayerBuildResult>(buildContext);
            if (!string.IsNullOrEmpty(result.Error))
            {
                Debug.LogError(result.Error);
                Debug.LogError($"Addressable content build failure (duration : {TimeSpan.FromSeconds(result.Duration).ToString("g")})");
            }
            else
                Debug.Log($"Addressable content successfully built (duration : {TimeSpan.FromSeconds(result.Duration).ToString("g")})");
            
            if (BuildScript.buildCompleted != null)
                BuildScript.buildCompleted(result);
            AssetDatabase.Refresh();
            return result;
        }

        /// <summary>
        /// Deletes all created runtime data for the active player data builder.
        /// </summary>
        /// <param name="builder">The builder to call ClearCachedData on.  If null, all builders will be cleaned</param>
        public static void CleanPlayerContent(IDataBuilder builder = null)
        {
            var settings = AddressableAssetSettingsDefaultObject.Settings;
            if (settings == null)
            {
                if (EditorApplication.isUpdating)
                    Debug.LogError("Addressable Asset Settings does not exist.  EditorApplication.isUpdating was true.");
                else if (EditorApplication.isCompiling)
                    Debug.LogError("Addressable Asset Settings does not exist.  EditorApplication.isCompiling was true.");
                else
                    Debug.LogError("Addressable Asset Settings does not exist.  Failed to create.");
                return;
            }
            settings.CleanPlayerContentImpl(builder);
        }

        internal void CleanPlayerContentImpl(IDataBuilder builder = null)
        {
            if (builder != null)
            {
                builder.ClearCachedData();
            }
            else
            {
                for (int i = 0; i < DataBuilders.Count; i++)
                {
                    var m = GetDataBuilder(i);
                    m.ClearCachedData();
                }
            }
            AssetDatabase.Refresh();
        }

        internal AsyncOperationHandle<IResourceLocator> CreatePlayModeInitializationOperation(AddressablesImpl addressables)
        {
            return addressables.ResourceManager.StartOperation(new FastModeInitializationOperation(addressables, this), default);
        }

        static Dictionary<string, Action<IEnumerable<AddressableAssetEntry>>> s_CustomAssetEntryCommands = new Dictionary<string, Action<IEnumerable<AddressableAssetEntry>>>();
        /// <summary>
        /// Register a custom command to process asset entries.  These commands will be shown in the context menu of the groups window.
        /// </summary>
        /// <param name="cmdId">The id of the command.  This will be used for the display name of the context menu item.</param>
        /// <param name="cmdFunc">The command handler function.</param>
        /// <returns>Returns true if the command was registered.</returns>
        public static bool RegisterCustomAssetEntryCommand(string cmdId, Action<IEnumerable<AddressableAssetEntry>> cmdFunc)
        {
            if (string.IsNullOrEmpty(cmdId))
            {
                Debug.LogError("RegisterCustomAssetEntryCommand - invalid command id.");
                return false;
            }
            if (cmdFunc == null)
            {
                Debug.LogError($"RegisterCustomAssetEntryCommand - command functor for id '{cmdId}'.");
                return false;
            }
            s_CustomAssetEntryCommands[cmdId] = cmdFunc;
            return true;
        }

        /// <summary>
        /// Removes a registered custom entry command.
        /// </summary>
        /// <param name="cmdId">The command id.</param>
        /// <returns>Returns true if the command was removed.</returns>
        public static bool UnregisterCustomAssetEntryCommand(string cmdId)
        {
            if (string.IsNullOrEmpty(cmdId))
            {
                Debug.LogError("UnregisterCustomAssetEntryCommand - invalid command id.");
                return false;
            }

            if (!s_CustomAssetEntryCommands.Remove(cmdId))
            {
                Debug.LogError($"UnregisterCustomAssetEntryCommand - command id '{cmdId}' is not registered.");
                return false;
            }
            return true;
        }

        /// <summary>
        /// Invoke a registered command for a set of entries.
        /// </summary>
        /// <param name="cmdId">The id of the command.</param>
        /// <param name="entries">The entries to run the command on.</param>
        /// <returns>Returns true if the command was executed without exceptions.</returns>
        public static bool InvokeAssetEntryCommand(string cmdId, IEnumerable<AddressableAssetEntry> entries)
        {
            try
            {
                if (string.IsNullOrEmpty(cmdId) || !s_CustomAssetEntryCommands.ContainsKey(cmdId))
                {
                    Debug.LogError($"Asset Entry Command '{cmdId}' not found.  Ensure that it is registered by calling RegisterCustomAssetEntryCommand.");
                    return false;
                }
                if (entries == null)
                {
                    Debug.LogError($"Asset Entry Command '{cmdId}' called with null entry collection.");
                    return false;
                }
                s_CustomAssetEntryCommands[cmdId](entries);
                return true;
            }
            catch (Exception e)
            {
                Debug.LogError($"Encountered exception when running Asset Entry Command '{cmdId}': {e.Message}");
                return false;
            }
        }

        /// <summary>
        /// The ids of the registered commands.
        /// </summary>
        public static IEnumerable<string> CustomAssetEntryCommands => s_CustomAssetEntryCommands.Keys;

        static Dictionary<string, Action<IEnumerable<AddressableAssetGroup>>> s_CustomAssetGroupCommands = new Dictionary<string, Action<IEnumerable<AddressableAssetGroup>>>();
        /// <summary>
        /// Register a custom command to process asset groups.  These commands will be shown in the context menu of the groups window.
        /// </summary>
        /// <param name="cmdId">The id of the command.  This will be used for the display name of the context menu item.</param>
        /// <param name="cmdFunc">The command handler function.</param>
        /// <returns>Returns true if the command was registered.</returns>
        public static bool RegisterCustomAssetGroupCommand(string cmdId, Action<IEnumerable<AddressableAssetGroup>> cmdFunc)
        {
            if (string.IsNullOrEmpty(cmdId))
            {
                Debug.LogError("RegisterCustomAssetGroupCommand - invalid command id.");
                return false;
            }
            if (cmdFunc == null)
            {
                Debug.LogError($"RegisterCustomAssetGroupCommand - command functor for id '{cmdId}'.");
                return false;
            }
            s_CustomAssetGroupCommands[cmdId] = cmdFunc;
            return true;
        }

        /// <summary>
        /// Removes a registered custom group command.
        /// </summary>
        /// <param name="cmdId">The command id.</param>
        /// <returns>Returns true if the command was removed.</returns>
        public static bool UnregisterCustomAssetGroupCommand(string cmdId)
        {
            if (string.IsNullOrEmpty(cmdId))
            {
                Debug.LogError("UnregisterCustomAssetGroupCommand - invalid command id.");
                return false;
            }
            if (!s_CustomAssetGroupCommands.Remove(cmdId))
            {
                Debug.LogError($"UnregisterCustomAssetGroupCommand - command id '{cmdId}' is not registered.");
                return false;
            }
            return true;
        }

        /// <summary>
        /// Invoke a registered command for a set of groups.
        /// </summary>
        /// <param name="cmdId">The id of the command.</param>
        /// <param name="groups">The groups to run the command on.</param>
        /// <returns>Returns true if the command was invoked successfully.</returns>
        public static bool InvokeAssetGroupCommand(string cmdId, IEnumerable<AddressableAssetGroup> groups)
        {
            try
            {
                if (string.IsNullOrEmpty(cmdId) || !s_CustomAssetGroupCommands.ContainsKey(cmdId))
                {
                    Debug.LogError($"Asset Group Command '{cmdId}' not found.  Ensure that it is registered by calling RegisterCustomAssetGroupCommand.");
                    return false;
                }
                if (groups == null)
                {
                    Debug.LogError($"Asset Group Command '{cmdId}' called with null group collection.");
                    return false;
                }
                s_CustomAssetGroupCommands[cmdId](groups);
                return true;
            }
            catch (Exception e)
            {
                Debug.LogError($"Encountered exception when running Asset Group Command '{cmdId}': {e.Message}");
                return false;
            }
        }

        /// <summary>
        /// The ids of the registered commands.
        /// </summary>
        public static IEnumerable<string> CustomAssetGroupCommands => s_CustomAssetGroupCommands.Keys;
    }
}

以上です。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?