using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
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.ResourceManagement.Util;
using UnityEngine.Serialization;
[assembly: InternalsVisibleTo("Unity.Addressables.Editor.Tests")]
[assembly: InternalsVisibleTo("Unity.Addressables.Tests")]
namespace UnityEditor.AddressableAssets.Settings
{
using Object = UnityEngine.Object;
///
/// Contains editor data for the addressables system.
///
public class AddressableAssetSettings : ScriptableObject
{
[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 = AddressableAssetSettingsDefaultObject.Settings.OnPostprocessAllAssets;
}
///
/// Default name of a newly created group.
///
public const string kNewGroupName = "New Group";
///
/// Default name of local build path.
///
public const string kLocalBuildPath = "LocalBuildPath";
///
/// Default name of local load path.
///
public const string kLocalLoadPath = "LocalLoadPath";
///
/// Default name of remote build path.
///
public const string kRemoteBuildPath = "RemoteBuildPath";
///
/// Default name of remote load path.
///
public const string kRemoteLoadPath = "RemoteLoadPath";
///
/// Enumeration of different event types that are generated.
///
public enum ModificationEvent
{
GroupAdded,
GroupRemoved,
GroupRenamed,
GroupSchemaAdded,
GroupSchemaRemoved,
GroupSchemaModified,
GroupTemplateAdded,
GroupTemplateRemoved,
GroupTemplateSchemaAdded,
GroupTemplateSchemaRemoved,
EntryCreated,
EntryAdded,
EntryMoved,
EntryRemoved,
LabelAdded,
LabelRemoved,
ProfileAdded,
ProfileRemoved,
ProfileModified,
ActiveProfileSet,
EntryModified,
BuildSettingsChanged,
ActiveBuildScriptChanged,
DataBuilderAdded,
DataBuilderRemoved,
InitializationObjectAdded,
InitializationObjectRemoved,
ActivePlayModeScriptChanged,
BatchModification, // <-- posted object will be null.
HostingServicesManagerModified,
GroupMoved,
CertificateHandlerChanged
}
///
/// The path of the settings asset.
///
public string AssetPath
{
get
{
string guid;
long localId;
if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(this, out guid, out localId))
return AddressableAssetSettingsDefaultObject.DefaultAssetPath;
var assetPath = AssetDatabase.GUIDToAssetPath(guid);
if (string.IsNullOrEmpty(assetPath))
return AddressableAssetSettingsDefaultObject.DefaultAssetPath;
return assetPath;
}
}
///
/// The folder of the settings asset.
///
public string ConfigFolder
{
get
{
return Path.GetDirectoryName(AssetPath);
}
}
///
/// The folder for the group assets.
///
public string GroupFolder
{
get
{
return ConfigFolder + "/AssetGroups";
}
}
///
/// The folder for the script assets.
///
public string DataBuilderFolder
{
get
{
return ConfigFolder + "/DataBuilders";
}
}
///
/// The folder for the asset group schema assets.
///
public string GroupSchemaFolder
{
get
{
return GroupFolder + "/Schemas";
}
}
///
/// The default folder for the group template assets.
///
public string GroupTemplateFolder
{
get
{
return ConfigFolder + "/AssetGroupTemplates";
}
}
///
/// Event for handling settings changes. The object passed depends on the event type.
///
public Action OnModification { get; set; }
///
/// Event for handling settings changes on all instances of AddressableAssetSettings. The object passed depends on the event type.
///
public static event Action OnModificationGlobal;
///
/// Event for handling the result of a DataBuilder.Build call.
///
public Action OnDataBuilderComplete { get; set; }
[FormerlySerializedAs("m_defaultGroup")]
[SerializeField]
string m_DefaultGroup;
[FormerlySerializedAs("m_cachedHash")]
[SerializeField]
Hash128 m_CachedHash;
bool m_IsTemporary;
///
/// Returns whether this settings object is persisted to an asset.
///
public bool IsPersisted { get { return !m_IsTemporary; } }
[SerializeField]
bool m_BuildRemoteCatalog = false;
///
/// Determine if a remote catalog should be built-for and loaded-by the app.
///
public bool BuildRemoteCatalog
{
get { return m_BuildRemoteCatalog; }
set { m_BuildRemoteCatalog = value; }
}
[SerializeField]
ProfileValueReference m_RemoteCatalogBuildPath;
///
/// 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.
///
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;
///
/// 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.
///
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; }
}
///
/// Hash of the current settings. This value is recomputed if anything changes.
///
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);
}
///
/// Create an AssetReference object. If the asset is not already addressable, it will be added.
///
/// The guid of the asset reference.
/// Returns the newly created AssetReference.
public AssetReference CreateAssetReference(string guid)
{
CreateOrMoveEntry(guid, DefaultGroup);
return new AssetReference(guid);
}
[SerializeField]
string m_overridePlayerVersion = "";
///
/// Allows for overriding the player version used to generated catalog names.
///
public string OverridePlayerVersion
{
get { return m_overridePlayerVersion; }
set { m_overridePlayerVersion = value; }
}
///
/// 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).
///
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 m_GroupAssets = new List();
///
/// List of asset groups.
///
public List groups { get { return m_GroupAssets; } }
[FormerlySerializedAs("m_buildSettings")]
[SerializeField]
AddressableAssetBuildSettings m_BuildSettings = new AddressableAssetBuildSettings();
///
/// Build settings object.
///
public AddressableAssetBuildSettings buildSettings { get { return m_BuildSettings; } }
[FormerlySerializedAs("m_profileSettings")]
[SerializeField]
AddressableAssetProfileSettings m_ProfileSettings = new AddressableAssetProfileSettings();
///
/// Profile settings object.
///
public AddressableAssetProfileSettings profileSettings { get { return m_ProfileSettings; } }
[FormerlySerializedAs("m_labelTable")]
[SerializeField]
LabelTable m_LabelTable = new LabelTable();
///
/// LabelTable object.
///
internal LabelTable labelTable { get { return m_LabelTable; } }
[FormerlySerializedAs("m_schemaTemplates")]
[SerializeField]
List m_SchemaTemplates = new List();
///
/// Remove the schema at the specified index.
///
/// The index to remove at.
/// Indicates if an even should be posted to the Addressables event system for this change.
/// True if the schema was removed.
public bool RemoveSchemaTemplate(int index, bool postEvent = true)
{
if (index < 0 || index >= m_SchemaTemplates.Count)
{
Debug.LogWarningFormat("Invalid index for schema template: {0}.", index);
return false;
}
var s = m_SchemaTemplates[index];
m_SchemaTemplates.RemoveAt(index);
SetDirty(ModificationEvent.GroupSchemaRemoved, s, postEvent, true);
return true;
}
[SerializeField]
List m_GroupTemplateObjects = new List();
///
/// List of ScriptableObjects that implement the IGroupTemplate interface for providing new templates.
/// For use in the AddressableAssetsWindow to display new groups to create
///
public List GroupTemplateObjects
{
get { return m_GroupTemplateObjects; }
}
///
/// Get the IGroupTemplate at the specified index.
///
/// The index of the template object.
/// The AddressableAssetGroupTemplate object at the specified index.
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;
}
///
/// Adds a AddressableAssetsGroupTemplate object.
///
/// The AddressableAssetGroupTemplate object to add.
/// Indicates if an even should be posted to the Addressables event system for this change.
/// True if the initialization object was added.
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;
}
///
/// Remove the AddressableAssetGroupTemplate object at the specified index.
///
/// The index to remove.
/// Indicates if an event should be posted to the Addressables event system for this change.
/// True if the initialization object was removed.
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;
}
///
/// Sets the initialization object at the specified index.
///
/// The index to set the initialization object.
/// The initialization object to set. This must be a valid scriptable object that implements the IInitializationObject interface.
/// Indicates if an even should be posted to the Addressables event system for this change.
/// True if the initialization object was set, false otherwise.
public bool SetGroupTemplateObjectAtIndex(int index, IGroupTemplate initObject, bool postEvent = true)
{
if (m_GroupTemplateObjects.Count <= index)
return false;
if (initObject == null)
{
Debug.LogWarning("Cannot add null IGroupTemplate");
return false;
}
var so = initObject 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 m_InitializationObjects = new List();
///
/// List of ScriptableObjects that implement the IObjectInitializationDataProvider interface for providing runtime initialization.
///
public List InitializationObjects
{
get { return m_InitializationObjects; }
}
///
/// Get the IObjectInitializationDataProvider at a specifc index.
///
/// The index of the initialization object.
/// The initialization object at the specified index.
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;
}
///
/// Adds an initialization object.
///
/// The initialization object to add.
/// Indicates if an even should be posted to the Addressables event system for this change.
/// True if the initialization object was added.
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;
}
///
/// Remove the initialization object at the specified index.
///
/// The index to remove.
/// Indicates if an even should be posted to the Addressables event system for this change.
/// True if the initialization object was removed.
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;
}
///
/// Sets the initialization object at the specified index.
///
/// The index to set the initialization object.
/// The initialization object to set. This must be a valid scriptable object that implements the IInitializationObject interface.
/// Indicates if an even should be posted to the Addressables event system for this change.
/// True if the initialization object was set, false otherwise.
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;
///
/// The type of CertificateHandler to use for this provider.
///
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 m_DataBuilders = new List();
///
/// List of ScriptableObjects that implement the IDataBuilder interface. These are used to create data for editor play mode and for player builds.
///
public List DataBuilders { get { return m_DataBuilders; } }
///
/// Get The data builder at a specifc index.
///
/// The index of the builder.
/// The data builder at the specified index.
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;
}
///
/// Adds a data builder.
///
/// The data builder to add.
/// Indicates if an even should be posted to the Addressables event system for this change.
/// True if the data builder was added.
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;
}
///
/// Remove the data builder at the sprcified index.
///
/// The index to remove.
/// Indicates if an even should be posted to the Addressables event system for this change.
/// True if the builder was removed.
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;
}
///
/// Sets the data builder at the specified index.
///
/// The index to set the builder.
/// The builder to set. This must be a valid scriptable object that implements the IDataBuilder interface.
/// Indicates if an even should be posted to the Addressables event system for this change.
/// True if the builder was set, false otherwise.
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;
}
///
/// Get the active data builder for player data.
///
public IDataBuilder ActivePlayerDataBuilder
{
get
{
return GetDataBuilder(m_ActivePlayerDataBuilderIndex);
}
}
///
/// Get the active data builder for editor play mode data.
///
public IDataBuilder ActivePlayModeDataBuilder
{
get
{
return GetDataBuilder(ProjectConfigData.activePlayModeIndex);
}
}
///
/// Get the index of the active player data builder.
///
public int ActivePlayerDataBuilderIndex
{
get
{
return m_ActivePlayerDataBuilderIndex;
}
set
{
m_ActivePlayerDataBuilderIndex = value;
SetDirty(ModificationEvent.ActiveBuildScriptChanged, ActivePlayerDataBuilder, true, true);
}
}
///
/// Get the index of the active play mode data builder.
///
public int ActivePlayModeDataBuilderIndex
{
get
{
return ProjectConfigData.activePlayModeIndex;
}
set
{
ProjectConfigData.activePlayModeIndex = value;
SetDirty(ModificationEvent.ActivePlayModeScriptChanged, ActivePlayModeDataBuilder, true, false);
}
}
///
/// Add a new label.
///
/// The label name.
/// Send modification event.
public void AddLabel(string label, bool postEvent = true)
{
m_LabelTable.AddLabelName(label);
SetDirty(ModificationEvent.LabelAdded, label, postEvent, true);
}
///
/// Remove a label by name.
///
/// The label name.
/// Send modification event.
public void RemoveLabel(string label, bool postEvent = true)
{
m_LabelTable.RemoveLabelName(label);
SetDirty(ModificationEvent.LabelRemoved, label, postEvent, true);
}
[FormerlySerializedAs("m_activeProfileId")]
[SerializeField]
string m_ActiveProfileId;
///
/// The active profile id.
///
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;
///
/// Get the HostingServicesManager object.
///
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; }
}
///
/// Gets all asset entries from all groups.
///
/// The list of asset entries.
/// A method to filter groups. Groups will be processed if filter is null, or it returns TRUE
/// A method to filter entries. Entries will be processed if filter is null, or it returns TRUE
public void GetAllAssets(List assets, Func groupFilter = null, Func entryFilter = null)
{
foreach (var g in groups)
if (groupFilter == null || groupFilter(g))
g.GatherAllAssets(assets, true, true, entryFilter);
}
///
/// Remove an asset entry.
///
/// The guid of the asset.
/// Send modifcation event.
/// True if the entry was found and removed.
public bool RemoveAssetEntry(string guid, bool postEvent = true)
{
var entry = FindAssetEntry(guid);
if (entry != null)
{
if (entry.parentGroup != null)
entry.parentGroup.RemoveAssetEntry(entry, postEvent);
SetDirty(ModificationEvent.EntryRemoved, entry, postEvent, false);
return true;
}
return false;
}
void OnEnable()
{
profileSettings.OnAfterDeserialize(this);
buildSettings.OnAfterDeserialize(this);
Validate();
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)
{
for (int i = m_SchemaTemplates.Count - 1; i >= 0; --i)
{
string assetPath = GroupTemplateFolder + "/" + m_SchemaTemplates[i].DisplayName + ".asset";
if (File.Exists(assetPath))
{
if(LoadGroupTemplateObject(this, assetPath))
m_SchemaTemplates.RemoveAt(i);
}
else
{
if (CreateAndAddGroupTemplate(m_SchemaTemplates[i].DisplayName,
m_SchemaTemplates[i].Description, m_SchemaTemplates[i].GetTypes()))
m_SchemaTemplates.RemoveAt(i);
}
}
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();
m_DataBuilders.Add(CreateScriptAsset());
m_DataBuilders.Add(CreateScriptAsset());
m_DataBuilders.Add(CreateScriptAsset());
m_DataBuilders.Add(CreateScriptAsset());
}
if (ActivePlayerDataBuilder != null && !ActivePlayerDataBuilder.CanBuildData())
ActivePlayerDataBuilderIndex = m_DataBuilders.IndexOf(m_DataBuilders.Find(s => s.GetType() == typeof(BuildScriptPackedMode)));
if (ActivePlayModeDataBuilder != null && !ActivePlayModeDataBuilder.CanBuildData())
ActivePlayModeDataBuilderIndex = m_DataBuilders.IndexOf(m_DataBuilders.Find(s => s.GetType() == typeof(BuildScriptFastMode)));
profileSettings.Validate(this);
buildSettings.Validate(this);
}
T CreateScriptAsset() where T : ScriptableObject
{
var script = CreateInstance();
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(path);
}
internal const string PlayerDataGroupName = "Built In Data";
internal const string DefaultLocalGroupName = "Default Local Group";
///
/// Create a new AddressableAssetSettings object.
///
/// The folder to store the settings object.
/// The name of the settings object.
/// If true, create groups for player data and local packed content.
/// If true, assets are created.
///
public static AddressableAssetSettings Create(string configFolder, string configName, bool createDefaultGroups, bool isPersisted)
{
AddressableAssetSettings aa;
var path = configFolder + "/" + configName + ".asset";
aa = isPersisted ? AssetDatabase.LoadAssetAtPath(path) : null;
if (aa == null)
{
aa = CreateInstance();
aa.m_IsTemporary = !isPersisted;
aa.activeProfileId = aa.profileSettings.Reset();
aa.name = configName;
if (isPersisted)
{
Directory.CreateDirectory(configFolder);
AssetDatabase.CreateAsset(aa, path);
}
if (createDefaultGroups)
{
CreateBuiltInData(aa);
CreateDefaultGroup(aa);
}
if (isPersisted)
AssetDatabase.SaveAssets();
}
return aa;
}
///
/// Creates a new AddressableAssetGroupTemplate Object with the set of schema types with default settings for use in the editor GUI.
///
/// The display name of the template.
/// Description text use with the template.
/// The schema types for the template.
/// True if the template was added, false otherwise.
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();
newAssetGroupTemplate.Description = description;
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;
}
///
/// Find asset group by functor.
///
/// The functor to call on each group. The first group that evaluates to true is returned.
/// The group found or null.
public AddressableAssetGroup FindGroup(Func func)
{
return groups.Find(g => func(g));
}
///
/// Find asset group by name.
///
/// The name of the group.
/// The group found or null.
public AddressableAssetGroup FindGroup(string groupName)
{
return FindGroup(g => g.Name == groupName);
}
///
/// The default group. This group is used when marking assets as addressable via the inspector.
///
public AddressableAssetGroup DefaultGroup
{
get
{
AddressableAssetGroup group = null;
if (string.IsNullOrEmpty(m_DefaultGroup))
group = groups.Find(s => s.CanBeSetAsDefault());
else
{
group = groups.Find(s => s.Guid == m_DefaultGroup);
if (group == null || !group.CanBeSetAsDefault())
{
group = groups.Find(s => 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;
}
}
private static AddressableAssetGroup CreateBuiltInData(AddressableAssetSettings aa)
{
var playerData = aa.CreateGroup(PlayerDataGroupName, false, true, false, null, typeof(PlayerDataGroupSchema));
var resourceEntry = aa.CreateOrMoveEntry(AddressableAssetEntry.ResourcesName, playerData);
resourceEntry.IsInResources = true;
aa.CreateOrMoveEntry(AddressableAssetEntry.EditorSceneListName, playerData);
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();
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);
}
AddressableAssetEntry CreateEntry(string guid, string address, AddressableAssetGroup parent, bool readOnly, bool postEvent = true)
{
var entry = new AddressableAssetEntry(guid, address, parent, readOnly);
if (!readOnly)
SetDirty(ModificationEvent.EntryCreated, entry, postEvent, true);
return entry;
}
///
/// 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 settings asset will be marked as dirty.
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);
}
///
/// Find and asset entry by guid.
///
/// The asset guid.
/// The found entry or null.
public AddressableAssetEntry FindAssetEntry(string guid)
{
foreach (var g in groups)
{
var e = g.GetAssetEntry(guid);
if (e != null)
return e;
}
return null;
}
internal void MoveAssetsFromResources(Dictionary guidToNewPath, AddressableAssetGroup targetParent)
{
if (guidToNewPath == null)
return;
var entries = new List();
AssetDatabase.StartAssetEditing();
foreach (var item in guidToNewPath)
{
var dirInfo = new FileInfo(item.Value).Directory;
if (dirInfo != null && !dirInfo.Exists)
{
dirInfo.Create();
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 = Path.GetFileNameWithoutExtension(oldPath.Substring(index + 10));
if (!string.IsNullOrEmpty(newAddress))
{
newEntry.address = newAddress;
}
}
entries.Add(newEntry);
}
}
AssetDatabase.StopAssetEditing();
AssetDatabase.Refresh();
SetDirty(ModificationEvent.EntryMoved, entries, true, true);
}
///
/// Move an existing entry to a group.
///
/// The entries to move.
/// The group to add the entries to.
/// Should the entries be read only.
/// Send modification event.
///
public void MoveEntries(List 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);
}
}
///
/// Move an existing entry to a group.
///
/// The entry to move.
/// The group to add the entry to.
/// Should the entry be read only.
/// Send modification event.
///
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);
}
///
/// Create a new entry, or if one exists in a different group, move it into the new group.
///
/// The asset guid.
/// The group to add the entry to.
/// Is the new entry read only.
/// Send modification event.
///
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
{
var path = AssetDatabase.GUIDToAssetPath(guid);
if (AddressableAssetUtility.IsPathValidForEntry(path))
{
entry = CreateEntry(guid, path, targetParent, readOnly, postEvent);
}
else
{
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;
var entry = FindAssetEntry(guid);
if (entry == null)
{
entry = CreateEntry(guid, address, parentEntry.parentGroup, true);
entry.IsSubAsset = true;
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;
return entry;
}
return null;
}
///
/// Create a new asset group.
///
/// The group name.
/// Set the new group as the default group.
/// Is the new group read only.
/// Post modification event.
/// Schema set to copy from.
/// Types of schemas to add.
/// The newly created group.
public AddressableAssetGroup CreateGroup(string groupName, bool setAsDefaultGroup, bool readOnly, bool postEvent, List schemasToCopy, params Type[] types)
{
if (string.IsNullOrEmpty(groupName))
groupName = kNewGroupName;
string validName = FindUniqueGroupName(groupName);
var group = CreateInstance();
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);
groups.Add(group);
if (setAsDefaultGroup)
DefaultGroup = group;
SetDirty(ModificationEvent.GroupAdded, group, postEvent, true);
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.Name == groupName)
{
foundExisting = true;
break;
}
}
return foundExisting;
}
///
/// Remove an asset group.
///
///
public void RemoveGroup(AddressableAssetGroup g)
{
RemoveGroupInternal(g, true, true);
}
internal void RemoveGroupInternal(AddressableAssetGroup g, bool deleteAsset, bool postEvent)
{
g.ClearSchemas(true);
groups.Remove(g);
SetDirty(ModificationEvent.GroupRemoved, g, postEvent, true);
if (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 entries, string label, bool value, bool postEvent = true)
{
if (value)
AddLabel(label);
foreach (var e in entries)
e.SetLabel(label, value, false);
SetDirty(ModificationEvent.EntryModified, entries, postEvent, true);
}
internal void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
{
var aa = this;
bool modified = false;
foreach (string str in importedAssets)
{
var assetType = AssetDatabase.GetMainAssetTypeAtPath(str);
if (typeof(AddressableAssetGroup).IsAssignableFrom(assetType))
{
AddressableAssetGroup group = aa.FindGroup(Path.GetFileNameWithoutExtension(str));
if (group != null)
group.DedupeEnteries();
}
if (typeof(AddressableAssetEntryCollection).IsAssignableFrom(assetType))
{
aa.CreateOrMoveEntry(AssetDatabase.AssetPathToGUID(str), aa.DefaultGroup);
modified = true;
}
var guid = AssetDatabase.AssetPathToGUID(str);
if (aa.FindAssetEntry(guid) != null)
modified = true;
if (AddressableAssetUtility.IsInResources(str))
modified = true;
}
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))
{
modified = true;
}
}
foreach (string str in deletedAssets)
{
if (AddressableAssetUtility.IsInResources(str))
modified = true;
else
{
if (CheckForGroupDataDeletion(str))
{
modified = true;
continue;
}
var guidOfDeletedAsset = AssetDatabase.AssetPathToGUID(str);
if (aa.RemoveAssetEntry(guidOfDeletedAsset))
{
modified = 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;
}
}
else
{
var guid = AssetDatabase.AssetPathToGUID(str);
bool isAlreadyAddressable = aa.FindAssetEntry(guid) != null;
bool startedInResources = AddressableAssetUtility.IsInResources(movedFromAssetPaths[i]);
bool endedInResources = AddressableAssetUtility.IsInResources(str);
bool inEditorSceneList = BuiltinSceneCache.Contains(new GUID(guid));
//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.
modified = isAlreadyAddressable || startedInResources || endedInResources || inEditorSceneList;
}
}
if (modified)
aa.SetDirty(ModificationEvent.BatchModification, null, true, true);
}
internal bool CheckForGroupDataDeletion(string str)
{
bool modified = false;
AddressableAssetGroup groupToDelete = null;
bool deleteGroup = false;
foreach (var group in groups)
{
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;
}
///
/// Runs the active player data build script to create runtime data.
///
public static void BuildPlayerContent()
{
var settings = AddressableAssetSettingsDefaultObject.Settings;
if (settings == null)
{
Debug.LogError("Addressable Asset Settings does not exist.");
return;
}
settings.BuildPlayerContentImpl();
}
internal void BuildPlayerContentImpl()
{
if (Directory.Exists(Addressables.BuildPath))
{
try
{
Directory.Delete(Addressables.BuildPath, true);
}
catch (Exception e)
{
Debug.LogException(e);
}
}
var buildContext = new AddressablesDataBuilderInput(this);
var result = ActivePlayerDataBuilder.BuildData(buildContext);
if (!string.IsNullOrEmpty(result.Error))
Debug.LogError(result.Error);
AddressableAnalytics.Report(this);
if (BuildScript.buildCompleted != null)
BuildScript.buildCompleted(result);
AssetDatabase.Refresh();
}
///
/// Deletes all created runtime data for the active player data builder.
///
/// The builder to call ClearCachedData on. If null, all builders will be cleaned
public static void CleanPlayerContent(IDataBuilder builder = null)
{
var settings = AddressableAssetSettingsDefaultObject.Settings;
if (settings == null)
{
Debug.LogError("Addressable Asset Settings does not exist.");
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();
}
}
}