using System; using System.Collections.Generic; using System.Linq; using UnityEditor.AddressableAssets.GUI; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.AddressableAssets.Initialization; using UnityEngine.Serialization; // ReSharper disable DelegateSubtraction namespace UnityEditor.AddressableAssets.Settings { /// /// Contains user defined variables to control build parameters. /// [Serializable] public class AddressableAssetProfileSettings { internal delegate string ProfileStringEvaluationDelegate(string key); [NonSerialized] internal ProfileStringEvaluationDelegate onProfileStringEvaluation; internal void RegisterProfileStringEvaluationFunc(ProfileStringEvaluationDelegate f) { onProfileStringEvaluation -= f; onProfileStringEvaluation += f; } internal void UnregisterProfileStringEvaluationFunc(ProfileStringEvaluationDelegate f) { onProfileStringEvaluation -= f; } [Serializable] internal class BuildProfile { [Serializable] internal class ProfileEntry { [FormerlySerializedAs("m_id")] [SerializeField] string m_Id; public string id { get { return m_Id; } set { m_Id = value; m_CurrentHash = default; } } [FormerlySerializedAs("m_value")] [SerializeField] string m_Value; Hash128 m_CurrentHash; internal Hash128 currentHash { get { if (!m_CurrentHash.isValid) { m_CurrentHash.Append(m_Id); m_CurrentHash.Append(m_Value); } return m_CurrentHash; } } public string value { get { return m_Value; } set { m_Value = value; m_CurrentHash = default; } } internal ProfileEntry() { } internal ProfileEntry(string id, string v) { m_Id = id; m_Value = v; } internal ProfileEntry(ProfileEntry copy) { m_Id = copy.m_Id; m_Value = copy.m_Value; } } [NonSerialized] internal AddressableAssetProfileSettings m_ProfileParent; [FormerlySerializedAs("m_inheritedParent")] [SerializeField] string m_InheritedParent; public string inheritedParent { get { return m_InheritedParent; } set { m_InheritedParent = value; m_CurrentHash = default; } } [FormerlySerializedAs("m_id")] [SerializeField] string m_Id; internal string id { get { return m_Id; } set { m_Id = value; m_CurrentHash = default; } } [FormerlySerializedAs("m_profileName")] [SerializeField] string m_ProfileName; internal string profileName { get { return m_ProfileName; } set { m_ProfileName = value; m_CurrentHash = default; } } [FormerlySerializedAs("m_values")] [SerializeField] List m_Values = new List(); Hash128 m_CurrentHash; internal Hash128 currentHash { get { if (!m_CurrentHash.isValid) { m_CurrentHash.Append(m_Id); m_CurrentHash.Append(m_ProfileName); m_CurrentHash.Append(m_InheritedParent); foreach (var e in m_Values) { var peh = e.currentHash; m_CurrentHash.Append(ref peh); } } return m_CurrentHash; } } internal List values { get { return m_Values; } set { m_Values = value; m_CurrentHash = default; } } internal BuildProfile(string name, BuildProfile copyFrom, AddressableAssetProfileSettings ps) { m_InheritedParent = null; id = GUID.Generate().ToString(); profileName = name; values.Clear(); m_ProfileParent = ps; if (copyFrom != null) { foreach (var v in copyFrom.values) values.Add(new ProfileEntry(v)); m_InheritedParent = copyFrom.m_InheritedParent; } } internal void OnAfterDeserialize(AddressableAssetProfileSettings ps) { m_ProfileParent = ps; } internal string GetValueById(string variableId) { var i = values.FindIndex(v => v.id == variableId); if (i >= 0) return values[i].value; if (m_ProfileParent == null) { return null; } return m_ProfileParent.GetValueById(m_InheritedParent, variableId); } internal void SetValueById(string variableId, string val) { var i = values.FindIndex(v => v.id == variableId); if (i >= 0) values[i].value = val; m_CurrentHash = default; } } internal void OnAfterDeserialize(AddressableAssetSettings settings) { m_Settings = settings; foreach (var prof in m_Profiles) { prof.OnAfterDeserialize(this); } } [NonSerialized] AddressableAssetSettings m_Settings; [FormerlySerializedAs("m_profiles")] [SerializeField] List m_Profiles = new List(); internal List profiles { get { return m_Profiles; } } /// /// A container for profile specific data, such as the name and ID of a profile. /// [Serializable] public class ProfileIdData { [FormerlySerializedAs("m_id")] [SerializeField] string m_Id; /// /// The unique ID set to identify a specific profile. /// public string Id { get { return m_Id; } } [FormerlySerializedAs("m_name")] [SerializeField] string m_Name; /// /// The name of the specific profile. /// public string ProfileName { get { return m_Name; } } Hash128 m_CurrentHash; internal Hash128 currentHash { get { if (!m_CurrentHash.isValid) { m_CurrentHash.Append(m_Id); m_CurrentHash.Append(m_Name); m_CurrentHash.Append(ref m_InlineUsage); } return m_CurrentHash; } } /// /// Changes the name of a given profile and updates the values in the profile settings. /// /// The new name you want to set the profile to. /// The profile settings object that contains this profile. public void SetName(string newName, AddressableAssetProfileSettings profileSettings) { if (!profileSettings.ValidateNewVariableName(newName)) return; var currRefStr = "[" + m_Name + "]"; var newRefStr = "[" + newName + "]"; m_Name = newName; foreach (var p in profileSettings.profiles) { foreach (var v in p.values) v.value = v.value.Replace(currRefStr, newRefStr); } m_CurrentHash = default; profileSettings.SetDirty(AddressableAssetSettings.ModificationEvent.ProfileModified, null, false); ProfileWindow.MarkForReload(); } [FormerlySerializedAs("m_inlineUsage")] [SerializeField] bool m_InlineUsage; internal bool InlineUsage { get { return m_InlineUsage; } } internal ProfileIdData() { } /// /// Create a new ProfileIdData. /// /// The unique ID for this ProfileIdData /// The name of the ProfileIdData. ProfileIdData names should be unique in a given AddressableAssetProfileSettings. /// False by default, this informs the BuildProifleSettingsEditor on if it should evaluate the ProfileIdData directly (true) /// or get the value by Id first before evaluation (false). public ProfileIdData(string entryId, string entryName, bool inline) { m_Id = entryId; m_Name = entryName; m_InlineUsage = inline; } } [FormerlySerializedAs("m_profileEntryNames")] [SerializeField] List m_ProfileEntryNames = new List(); [FormerlySerializedAs("m_profileVersion")] [SerializeField] internal int m_ProfileVersion; const int k_CurrentProfileVersion = 1; Hash128 m_CurrentHash; internal Hash128 currentHash { get { if (!m_CurrentHash.isValid) { m_CurrentHash.Append(m_ProfileVersion); foreach (var p in m_ProfileEntryNames) { var peh = p.currentHash; m_CurrentHash.Append(ref peh); } foreach (var p in m_Profiles) { var peh = p.currentHash; m_CurrentHash.Append(ref peh); } } return m_CurrentHash; } } internal List profileEntryNames { get { if (m_ProfileVersion < k_CurrentProfileVersion) { m_ProfileVersion = k_CurrentProfileVersion; //migration cleanup from old way of doing "custom" values... var removeId = string.Empty; foreach (var idPair in m_ProfileEntryNames) { if (idPair.ProfileName == customEntryString) { removeId = idPair.Id; break; } } if (!string.IsNullOrEmpty(removeId)) RemoveValue(removeId); } return m_ProfileEntryNames; } } /// /// Text that represents a custom profile entry. /// public const string customEntryString = ""; /// /// Text that represents when the default settings path is being used. /// public const string defaultSettingsPathString = ""; /// /// Text that represents an undefined profile entry. /// public const string undefinedEntryValue = ""; const string k_RootProfileName = "Default"; /// /// Get the profile specific data for a given profile id. /// /// The profile id you're requesting data for. /// A ProfileIdData with information about a specific profile. public ProfileIdData GetProfileDataById(string id) { return profileEntryNames.Find(p => p.Id == id); } /// /// Get the profile specific data for a given profile name. /// /// The profile name you're requesting data for. /// A ProfileIdData with information about a specific profile. public ProfileIdData GetProfileDataByName(string name) { return profileEntryNames.Find(p => p.ProfileName == name); } /// /// Clears out the list of profiles, then creates a new default one. /// /// Returns the ID of the newly created default profile. public string Reset() { m_Profiles = new List(); m_CurrentHash = default; return CreateDefaultProfile(); } /// /// Evaluate a string given a profile id. /// /// The profile id to use for evaluation. /// The string to evaluate. Any tokens surrounded by '[' and ']' will be replaced with matching variables. /// The evaluated string. public string EvaluateString(string profileId, string varString) { Func getVal = s => { var v = GetValueByName(profileId, s); if (string.IsNullOrEmpty(v)) { if (onProfileStringEvaluation != null) { foreach (var i in onProfileStringEvaluation.GetInvocationList()) { var del = (ProfileStringEvaluationDelegate)i; v = del(s); if (!string.IsNullOrEmpty(v)) return v; } } v = AddressablesRuntimeProperties.EvaluateProperty(s); } return v; }; return AddressablesRuntimeProperties.EvaluateString(varString, '[', ']', getVal); } internal void Validate(AddressableAssetSettings addressableAssetSettings) { CreateDefaultProfile(); } internal string CreateDefaultProfile() { if (!ValidateProfiles()) { m_ProfileEntryNames.Clear(); m_Profiles.Clear(); AddProfile(k_RootProfileName, null); CreateValue("BuildTarget", "[UnityEditor.EditorUserBuildSettings.activeBuildTarget]"); CreateValue(AddressableAssetSettings.kLocalBuildPath, AddressableAssetSettings.kLocalBuildPathValue); CreateValue(AddressableAssetSettings.kLocalLoadPath, AddressableAssetSettings.kLocalLoadPathValue); CreateValue(AddressableAssetSettings.kRemoteBuildPath, AddressableAssetSettings.kRemoteBuildPathValue); CreateValue(AddressableAssetSettings.kRemoteLoadPath, AddressableAssetSettings.RemoteLoadPathValue); } return GetDefaultProfileId(); } string GetDefaultProfileId() { var def = GetDefaultProfile(); if (def != null) return def.id; return null; } BuildProfile GetDefaultProfile() { BuildProfile profile = null; if (m_Profiles.Count > 0) profile = m_Profiles[0]; return profile; } bool ValidateProfiles() { if (m_Profiles.Count == 0) return false; var root = m_Profiles[0]; if (root == null || root.values == null) return false; foreach (var i in profileEntryNames) if (string.IsNullOrEmpty(i.Id) || string.IsNullOrEmpty(i.ProfileName)) return false; var rootValueCount = root.values.Count; for (int index = 1; index < m_Profiles.Count; index++) { var profile = m_Profiles[index]; if (profile == null || string.IsNullOrEmpty(profile.profileName)) return false; if (profile.values == null || profile.values.Count != rootValueCount) return false; for (int i = 0; i < rootValueCount; i++) { if (root.values[i].id != profile.values[i].id) return false; } } return true; } /// /// Get all available variable names /// /// The variable names, sorted alphabetically. public List GetVariableNames() { HashSet names = new HashSet(); foreach (var entry in profileEntryNames) names.Add(entry.ProfileName); var list = names.ToList(); list.Sort(); return list; } /// /// Get all profile names. /// /// The list of profile names. public List GetAllProfileNames() { CreateDefaultProfile(); List result = new List(); foreach (var p in m_Profiles) result.Add(p.profileName); return result; } /// /// Get a profile's display name. /// /// The profile id. /// The display name of the profile. Returns empty string if not found. public string GetProfileName(string profileId) { foreach (var p in m_Profiles) { if (p.id == profileId) return p.profileName; } return ""; } /// /// Get the id of a given display name. /// /// The profile name. /// The id of the profile. Returns empty string if not found. public string GetProfileId(string profileName) { foreach (var p in m_Profiles) { if (p.profileName == profileName) return p.id; } return ""; } /// /// Gets the set of all profile ids. /// /// The set of profile ids. public HashSet GetAllVariableIds() { HashSet ids = new HashSet(); foreach (var v in profileEntryNames) ids.Add(v.Id); return ids; } /// /// 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. public void SetDirty(AddressableAssetSettings.ModificationEvent modificationEvent, object eventData, bool postEvent) { m_CurrentHash = default; if (m_Settings != null) m_Settings.SetDirty(modificationEvent, eventData, postEvent, true); } internal bool ValidateNewVariableName(string name) { foreach (var idPair in profileEntryNames) if (idPair.ProfileName == name) return false; return !string.IsNullOrEmpty(name) && !name.Any(c => { return c == '[' || c == ']' || c == '{' || c == '}'; }); } /// /// Adds a new profile. /// /// The name of the new profile. /// The id of the profile to copy values from. /// The id of the created profile. public string AddProfile(string name, string copyFromId) { var existingProfile = GetProfileByName(name); if (existingProfile != null) return existingProfile.id; var copyRoot = GetProfile(copyFromId); if (copyRoot == null && m_Profiles.Count > 0) copyRoot = GetDefaultProfile(); var prof = new BuildProfile(name, copyRoot, this); m_Profiles.Add(prof); SetDirty(AddressableAssetSettings.ModificationEvent.ProfileAdded, prof, true); ProfileWindow.MarkForReload(); return prof.id; } // Allows passing in the profile directly for internal methods, makes the profile window's code a bit cleaner // Can't be public since BuildProfile is an internal class internal bool RenameProfile(BuildProfile profile, string newName) { if (profile == null) { Addressables.LogError("Profile rename failed because profile passed in is null"); return false; } if (profile == GetDefaultProfile()) { Addressables.LogError("Profile rename failed because default profile cannot be renamed."); return false; } if (profile.profileName == newName) return false; // new name cannot only contain spaces if (newName.Trim().Length == 0) { Addressables.LogError("Profile rename failed because new profile name must not be only spaces."); return false; } bool profileExistsInSettingsList = false; for (int i = 0; i < m_Profiles.Count; i++) { // return false if there already exists a profile with the new name, no duplicates are allowed if (m_Profiles[i].profileName == newName) { Addressables.LogError("Profile rename failed because new profile name is not unique."); return false; } if (m_Profiles[i].id == profile.id) { profileExistsInSettingsList = true; } } // Rename the profile profile.profileName = newName; if (profileExistsInSettingsList) SetDirty(AddressableAssetSettings.ModificationEvent.ProfileModified, profile, true); ProfileWindow.MarkForReload(); return true; } /// /// Renames a profile. profileId must refer to an existing profile. Profile names must be unique and must not be comprised of only whitespace. /// Returns false if profileId or newName is invalid. /// /// The id of the profile to be renamed. /// The new name to be given to the profile. /// True if the rename is successful, false otherwise. public bool RenameProfile(string profileId, string newName) { var profileToRename = GetProfile(profileId); if (profileToRename == null) { Addressables.LogError("Profile rename failed because profile with sought id does not exist."); return false; } return RenameProfile(profileToRename, newName); } /// /// Removes a profile. /// /// The id of the profile to remove. public void RemoveProfile(string profileId) { m_Profiles.RemoveAll(p => p.id == profileId); m_Profiles.ForEach(p => { if (p.inheritedParent == profileId) p.inheritedParent = null; }); SetDirty(AddressableAssetSettings.ModificationEvent.ProfileRemoved, profileId, true); ProfileWindow.MarkForReload(); } BuildProfile GetProfileByName(string profileName) { return m_Profiles.Find(p => p.profileName == profileName); } internal string GetUniqueProfileName(string name) { return GenerateUniqueName(name, m_Profiles.Select(p => p.profileName)); } internal BuildProfile GetProfile(string profileId) { return m_Profiles.Find(p => p.id == profileId); } internal string GetVariableId(string variableName) { foreach (var idPair in profileEntryNames) { if (idPair.ProfileName == variableName) return idPair.Id; } return null; } /// /// Set the value of a variable for a specified profile. /// /// The profile id. /// The property name. /// The value to set the property. public void SetValue(string profileId, string variableName, string val) { var profile = GetProfile(profileId); if (profile == null) { Addressables.LogError("setting variable " + variableName + " failed because profile " + profileId + " does not exist."); return; } var id = GetVariableId(variableName); if (string.IsNullOrEmpty(id)) { Addressables.LogError("setting variable " + variableName + " failed because variable does not yet exist. Call CreateValue() first."); return; } profile.SetValueById(id, val); SetDirty(AddressableAssetSettings.ModificationEvent.ProfileModified, profile, true); ProfileWindow.MarkForReload(); } internal string GetUniqueProfileEntryName(string name) { return GenerateUniqueName(name, profileEntryNames.Select(p => p.ProfileName)); } /// /// Create a new profile property. /// /// The name of the property. /// The default value. /// The id of the created variable. public string CreateValue(string variableName, string defaultValue) { return CreateValue(variableName, defaultValue, false); } internal string CreateValue(string variableName, string defaultValue, bool inline) { if (m_Profiles.Count == 0) { Addressables.LogError("Attempting to add a profile variable in Addressables, but there are no profiles yet."); } var id = GetVariableId(variableName); if (string.IsNullOrEmpty(id)) { id = GUID.Generate().ToString(); profileEntryNames.Add(new ProfileIdData(id, variableName, inline)); foreach (var pro in m_Profiles) { pro.values.Add(new BuildProfile.ProfileEntry(id, defaultValue)); } } SetDirty(AddressableAssetSettings.ModificationEvent.ProfileModified, null, true); return id; } /// /// Remove a profile property. /// /// The id of the property. public void RemoveValue(string variableId) { foreach (var pro in m_Profiles) { pro.values.RemoveAll(x => x.id == variableId); } m_ProfileEntryNames.RemoveAll(x => x.Id == variableId); SetDirty(AddressableAssetSettings.ModificationEvent.ProfileModified, null, false); ProfileWindow.MarkForReload(); } /// /// Get the value of a property. /// /// The profile id. /// The property id. /// public string GetValueById(string profileId, string varId) { BuildProfile profile = GetProfile(profileId); return profile == null ? varId : profile.GetValueById(varId); } /// /// Get the value of a property by name. /// /// The profile id. /// The variable name. /// public string GetValueByName(string profileId, string varName) { return GetValueById(profileId, GetVariableId(varName)); } internal static string GenerateUniqueName(string baseName, IEnumerable enumerable) { var set = new HashSet(enumerable); int counter = 1; var newName = baseName; while (set.Contains(newName)) { newName = baseName + counter; counter++; if (counter == int.MaxValue) throw new OverflowException(); } return newName; } internal void CreateDuplicateVariableWithNewName(AddressableAssetSettings addressableAssetSettings, string newVariableName, string variableNameToCopyFrom) { var activeProfileId = addressableAssetSettings.activeProfileId; string newVarId = CreateValue(newVariableName, GetValueByName(activeProfileId, variableNameToCopyFrom)); string oldVarId = GetVariableId(variableNameToCopyFrom); foreach (var profile in profiles) { profile.SetValueById(newVarId, profile.GetValueById(oldVarId)); SetDirty(AddressableAssetSettings.ModificationEvent.ProfileModified, profile, true); } } } }