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);
}
}
}