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.SceneManagement; using UnityEngine.Serialization; namespace UnityEditor.AddressableAssets.Settings { /// /// Contains the collection of asset entries associated with this group. /// [Serializable] public class AddressableAssetGroup : ScriptableObject, IComparer, 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 m_SerializeEntries = new List(); [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 m_EntryMap = new Dictionary(); List m_FolderEntryCache = null; List m_AssetCollectionEntryCache = null; /// /// If true, this Group is likely marked 'Cannot Change Post Release', but has a modified asset since the previous build. /// public bool FlaggedDuringContentUpdateRestriction { get; internal set; } internal void RefreshEntriesCache() { m_FolderEntryCache = new List(); m_AssetCollectionEntryCache = new List(); FlaggedDuringContentUpdateRestriction = false; 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", StringComparison.OrdinalIgnoreCase) && e.MainAssetType == typeof(AddressableAssetEntryCollection)) m_AssetCollectionEntryCache.Add(e); #pragma warning restore 0618 if (e.FlaggedDuringContentUpdateRestriction) FlaggedDuringContentUpdateRestriction = true; } } /// /// The group name. /// 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); } } } /// /// The group GUID. /// public virtual string Guid { get { if (string.IsNullOrEmpty(m_GUID)) m_GUID = GUID.Generate().ToString(); return m_GUID; } } /// /// List of schemas for this group. /// public List Schemas { get { return m_SchemaSet.Schemas; } } /// /// Get the types of added schema for this group. /// public List SchemaTypes { get { return m_SchemaSet.Types; } } string GetSchemaAssetPath(Type type) { return Settings.IsPersisted ? (Settings.GroupSchemaFolder + "/" + Name + "_" + type.Name + ".asset") : string.Empty; } /// /// Adds a copy of the provided schema object. /// /// The schema to add. A copy will be made and saved in a folder relative to the main Addressables settings asset. /// Determines if this method call will post an event to the internal addressables event system /// The created schema object. 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; } /// /// 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. /// /// The schema type. This type must not already be added. /// Determines if this method call will post an event to the internal addressables event system /// The created schema object. 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; } /// /// Creates and adds a schema of a given type to this group. /// /// Determines if this method call will post an event to the internal addressables event system /// The schema type. This type must not already be added. /// The created schema object. public TSchema AddSchema(bool postEvent = true) where TSchema : AddressableAssetGroupSchema { return AddSchema(typeof(TSchema), postEvent) as TSchema; } /// /// Remove a given schema from this group. /// /// The schema type. /// Determines if this method call will post an event to the internal addressables event system /// True if the schema was found and removed, false otherwise. public bool RemoveSchema(Type type, bool postEvent = true) { if (!m_SchemaSet.RemoveSchema(type)) return false; SetDirty(AddressableAssetSettings.ModificationEvent.GroupSchemaRemoved, this, postEvent, true); return true; } /// /// Remove a given schema from this group. /// /// Determines if this method call will post an event to the internal addressables event system /// The schema type. /// True if the schema was found and removed, false otherwise. public bool RemoveSchema(bool postEvent = true) { return RemoveSchema(typeof(TSchema), postEvent); } /// /// Gets an added schema of the specified type. /// /// The schema type. /// The schema if found, otherwise null. public TSchema GetSchema() where TSchema : AddressableAssetGroupSchema { return GetSchema(typeof(TSchema)) as TSchema; } /// /// Gets an added schema of the specified type. /// /// The schema type. /// The schema if found, otherwise null. public AddressableAssetGroupSchema GetSchema(Type type) { return m_SchemaSet.GetSchema(type); } /// /// Checks if the group contains a schema of a given type. /// /// The schema type. /// True if the schema type or subclass has been added to this group. public bool HasSchema() { return HasSchema(typeof(TSchema)); } /// /// Removes all schemas and optionally deletes the assets associated with them. /// /// If true, the schema assets will also be deleted. /// Determines if this method call will post an event to the internal addressables event system public void ClearSchemas(bool deleteAssets, bool postEvent = true) { m_SchemaSet.ClearSchemas(deleteAssets); SetDirty(AddressableAssetSettings.ModificationEvent.GroupRemoved, this, postEvent, true); } /// /// Checks if the group contains a schema of a given type. /// /// The schema type. /// True if the schema type or subclass has been added to this group. public bool HasSchema(Type type) { return GetSchema(type) != null; } /// /// 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. /// public virtual bool ReadOnly { get { return m_ReadOnly; } } /// /// The AddressableAssetSettings that this group belongs to. /// public AddressableAssetSettings Settings { get { if (m_Settings == null) m_Settings = AddressableAssetSettingsDefaultObject.Settings; return m_Settings; } } /// /// The collection of asset entries. /// public virtual ICollection entries { get { return m_EntryMap.Values; } } internal Dictionary EntryMap => m_EntryMap; internal ICollection FolderEntries { get { if (m_FolderEntryCache == null) RefreshEntriesCache(); return m_FolderEntryCache; } } internal ICollection AssetCollectionEntries { get { if (m_AssetCollectionEntryCache == null) RefreshEntriesCache(); return m_AssetCollectionEntryCache; } } /// /// Is the default group. /// public virtual bool Default { get { return Guid == Settings.DefaultGroup.Guid; } } /// /// Compares two asset entries based on their guids. /// /// The first entry to compare. /// The second entry to compare. /// 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. 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); } Hash128 m_CurrentHash; internal Hash128 currentHash { get { if (!m_CurrentHash.isValid) { m_CurrentHash.Append(m_GroupName); m_CurrentHash.Append(m_GUID); m_CurrentHash.Append(entries.Count); m_CurrentHash.Append(ref m_ReadOnly); foreach (var e in entries) { var eh = e.currentHash; m_CurrentHash.Append(ref eh); } } return m_CurrentHash; } } /// /// Converts data to serializable format. /// public void OnBeforeSerialize() { if (m_SerializeEntries == null) { m_SerializeEntries = new List(entries.Count); foreach (var e in entries) m_SerializeEntries.Add(e); } } /// /// Converts data from serializable format. /// public void OnAfterDeserialize() { ResetEntryMap(); } internal void ResetEntryMap() { 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()) AddSchema(); 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 removeEntries = new List(); 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; } /// /// Gathers all asset entries. Each explicit entry may contain multiple sub entries. For example, addressable folders create entries for each asset contained within. /// /// The generated list of entries. For simple entries, this will contain just the entry itself if specified. /// Determines if the entry should be contained in the result list or just sub entries. /// Determines if full recursion should be done when gathering entries. /// Determines if sub objects such as sprites should be included. /// Optional predicate to run against each entry, only returning those that pass. A null filter will return all entries public virtual void GatherAllAssets(List results, bool includeSelf, bool recurseAll, bool includeSubObjects, Func entryFilter = null) { foreach (var e in entries) if (entryFilter == null || entryFilter(e)) e.GatherAllAssets(results, includeSelf, recurseAll, includeSubObjects, entryFilter); } internal void GatherAllDirectAssetReferenceEntryData(List results, HashSet processed) { if (processed == null) processed = new HashSet(); // go through all entries that are not in folder/collections foreach (AddressableAssetEntry entry in entries) { var path = entry.AssetPath; if (string.IsNullOrEmpty(path)) return; if (processed.Contains(path)) continue; if (FolderEntries.Contains(entry)) continue; if (entry.guid == AddressableAssetEntry.EditorSceneListName || entry.guid == AddressableAssetEntry.ResourcesName) continue; processed.Add(path); var reference = new ImplicitAssetEntry() { address = entry.address, AssetPath = entry.AssetPath, IsInResources = entry.IsInResources, labels = new HashSet(entry.labels) }; results.Add(reference); } } internal void GatherAllFolderSubAssetReferenceEntryData(List results, HashSet processed) { if (processed == null) processed = new HashSet(); // go through all entries that are in folder foreach (AddressableAssetEntry folderEntry in FolderEntries) { var path = folderEntry.AssetPath; if (string.IsNullOrEmpty(path)) return; if (processed.Contains(path)) continue; if (folderEntry.guid == AddressableAssetEntry.EditorSceneListName || folderEntry.guid == AddressableAssetEntry.ResourcesName) continue; processed.Add(folderEntry.AssetPath); string[] guids = AssetDatabase.FindAssets("", new[] {folderEntry.AssetPath}); foreach (var guid in guids) { string assetPath = AssetDatabase.GUIDToAssetPath(guid); if (string.IsNullOrEmpty(assetPath)) return; if (processed.Contains(assetPath)) continue; processed.Add(assetPath); if (BuiltinSceneCache.Contains(assetPath)) continue; if (AssetDatabase.IsValidFolder(assetPath)) continue; if (!AddressableAssetUtility.IsPathValidForEntry(assetPath)) continue; string relativePath = assetPath.Remove(0, folderEntry.AssetPath.Length); string relativeAddress = folderEntry.address + relativePath; var reference = new ImplicitAssetEntry() { address = relativeAddress, AssetPath = assetPath, IsInResources = folderEntry.IsInResources, labels = new HashSet(folderEntry.labels) }; results.Add(reference); } } } #pragma warning disable 0618 internal void GatherAllAssetCollectionAssetReferenceEntryData(List results, HashSet processed) { if (processed == null) processed = new HashSet(); if (AssetCollectionEntries.Count != 0) { foreach (var e in m_AssetCollectionEntryCache) { var entries = new List(); e.GatherAssetEntryCollectionEntries(entries, null); foreach (AddressableAssetEntry entry in entries) { // do entries string assetPath = entry.AssetPath; if (string.IsNullOrEmpty(assetPath)) return; if (processed.Contains(assetPath)) continue; processed.Add(assetPath); if (AssetDatabase.IsValidFolder(assetPath)) continue; if (!AddressableAssetUtility.IsPathValidForEntry(assetPath)) continue; var reference = new ImplicitAssetEntry() { address = entry.address, AssetPath = entry.AssetPath, IsInResources = entry.IsInResources, labels = new HashSet(entry.labels) }; results.Add(reference); } } } } #pragma warning restore 0618 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", StringComparison.OrdinalIgnoreCase) && e.MainAssetType == typeof(AddressableAssetEntryCollection)) m_AssetCollectionEntryCache.Add(e); #pragma warning restore 0618 if (HasSchema() && !GetSchema().StaticContent) e.FlaggedDuringContentUpdateRestriction = false; m_SerializeEntries = null; SetDirty(AddressableAssetSettings.ModificationEvent.EntryAdded, e, postEvent, true); } /// /// Get an entry via the asset guid. /// /// The asset guid. /// public virtual AddressableAssetEntry GetAssetEntry(string guid) { return GetAssetEntry(guid, false); } /// /// Get an entry via the asset guid. /// /// The asset guid. /// Whether or not to include implicit asset entries in the search. /// 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; } /// /// Marks the object as modified. /// /// The event type that is changed. /// The object data that corresponds to the event. /// If true, the event is propagated to callbacks. /// If true, the group asset will be marked as dirty. public void SetDirty(AddressableAssetSettings.ModificationEvent modificationEvent, object eventData, bool postEvent, bool groupModified = false) { m_CurrentHash = default; if (Settings != null) { if (groupModified && Settings.IsPersisted && this != null) EditorUtility.SetDirty(this); Settings.SetDirty(modificationEvent, eventData, postEvent, false); } } /// /// Remove an entry. /// /// The entry to remove. /// If true, post the event to callbacks. 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 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); } } /// /// Check to see if a group is the Default Group. /// /// public bool IsDefaultGroup() { return Guid == m_Settings.DefaultGroup.Guid; } /// /// Check if a group has the appropriate schemas and attributes that the Default Group requires. /// /// public bool CanBeSetAsDefault() { return !m_ReadOnly; } /// /// Gets the index of a schema based on its specified type. /// /// The schema type. /// Valid index if found, otherwise returns -1. 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); } } }