Addressablesの内部データをソート

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をインポート
1. `[プロジェクトフォルダ]/Library/PackageCache/com.unity.addressables@1.20.0`を`[プロジェクトフォルダ]/Packages`にコピー

## グループやエントリーの追加削除時にソートするようにコードを追記
変更するファイルは二つです。追記部分は`yukimoto`で検索。
やり方に不備はあるかもしれませんが、とりあえずこれで動いているのでヨシとします。
### AddressableAssetGroup.cs
グループ内エントリーに変更があった時、アドレス順にソートするようにします。
<details><summary>AddressableAssetGroup.cs</summary><div>

```diff:./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);
        }
    }
}
```

</div></details>

### AddressableAssetSettings.cs
こちらはグループのソートを行うように追記します。
<details><summary>AddressableAssetSettings.cs</summary><div>

```diff:./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;
    }
}

```

</div></details>

以上です。