using System; using System.Collections.Generic; using System.Linq; using UnityEditor.AddressableAssets.Settings; using UnityEditor.AddressableAssets.Settings.GroupSchemas; using UnityEngine; using UnityEngine.Localization; using UnityEngine.Localization.SmartFormat; using Object = UnityEngine.Object; namespace UnityEditor.Localization.Addressables { /// /// Provides support for placing assets into different s. /// /// /// This example places all English assets into a local group and all other languages into a remote group which could then be downloaded after the game is released. /// /// [Serializable] public class GroupResolver { [Serializable] class LocaleGroupPair { public LocaleIdentifier localeIdentifier; public AddressableAssetGroup group; } [SerializeField] string m_SharedGroupName = "Localization-Assets-Shared"; [SerializeField] AddressableAssetGroup m_SharedGroup; [SerializeField] string m_LocaleGroupNamePattern = "Localization-Assets-{LocaleName}"; [SerializeField] List m_LocaleGroups = new List(); [SerializeField] bool m_MarkEntriesReadOnly = true; /// /// The name to use when creating a new group. /// public string SharedGroupName { get => m_SharedGroupName; set => m_SharedGroupName = value; } /// /// The group to use for shared assets. If null then a new group will be created using . /// public AddressableAssetGroup SharedGroup { get => m_SharedGroup; set => m_SharedGroup = value; } /// /// The name to use when generating a new for a . /// public string LocaleGroupNamePattern { get => m_LocaleGroupNamePattern; set => m_LocaleGroupNamePattern = value; } /// /// Should new Entries be marked as read only? This will prevent editing them in the Addressables window. /// public bool MarkEntriesReadOnly { get => m_MarkEntriesReadOnly; set => m_MarkEntriesReadOnly = value; } /// /// Creates a new default instance of . /// public GroupResolver() {} /// /// Creates a new instance which will store all assets into a single group. /// /// The name to use when creating a new group. public GroupResolver(string groupName) { m_SharedGroupName = groupName; m_LocaleGroupNamePattern = groupName; } /// /// Creates a new instance which will store all assets into a single group; /// /// The group to use. public GroupResolver(AddressableAssetGroup group) { m_SharedGroup = group; m_SharedGroupName = group.Name; m_LocaleGroupNamePattern = group.Name; } /// /// Creates an instance using custom group names for each Locale. /// /// The name to use when creating a new Group for a selected . The name is formatted using where the argument passed will be the . The default is "Localization-Assets-{Code}"." /// The name of the group to use when an asset is shared by multiple Locales that do not all use the same group. public GroupResolver(string localeGroupNamePattern, string sharedGroupName) { m_SharedGroupName = sharedGroupName; m_LocaleGroupNamePattern = localeGroupNamePattern; } /// /// Add a group for the . If a Group is already assigned it will be replaced with the new group. /// /// The Locale Id to use for the selected group. /// The group to add for the selected Locale Id, must not be . public void AddLocaleGroup(LocaleIdentifier identifier, AddressableAssetGroup group) { if (group == null) throw new ArgumentNullException(nameof(group)); var expectedGroupPair = m_LocaleGroups.FirstOrDefault(g => g.localeIdentifier == identifier); if (expectedGroupPair == null) { expectedGroupPair = new LocaleGroupPair { localeIdentifier = identifier }; m_LocaleGroups.Add(expectedGroupPair); } expectedGroupPair.group = group; } /// /// Removes the group for the chosen Locale Id. /// /// The Locale Id to be removed. /// Returns if an item was removed. public bool RemoveLocaleGroup(LocaleIdentifier identifier) => m_LocaleGroups.RemoveAll(g => g.localeIdentifier == identifier) > 0; /// /// Returns the active group assigned to the or if one has not been assigned. /// /// The Locale Id to search for. /// public AddressableAssetGroup GetLocaleGroup(LocaleIdentifier identifier) => m_LocaleGroups.FirstOrDefault(g => g.localeIdentifier == identifier)?.group; /// /// Add an asset to an . /// The asset will be moved into a group which either matches or . /// /// The asset to be added to a group. /// List of locales that depend on this asset or null if the asset is used by all. /// The Addressables setting that will be used if a new group should be added. /// Should an Undo record be created? /// The asset entry for the added asset. public virtual AddressableAssetEntry AddToGroup(Object asset, IList locales, AddressableAssetSettings aaSettings, bool createUndo) { var group = SharedGroup ?? GetGroup(locales, asset, aaSettings, createUndo); var guid = GetAssetGuid(asset); var assetEntry = aaSettings.FindAssetEntry(guid); if (assetEntry == null) { if (createUndo) Undo.RecordObjects(new Object[] { aaSettings, group }, "Add to group"); assetEntry = aaSettings.CreateOrMoveEntry(guid, group, MarkEntriesReadOnly); } else { // TODO: We may want to provide an option to leave assets that are in unknown groups here. We would need to figure out a way to know what is a known group and what is not. if (createUndo) Undo.RecordObjects(new Object[] { aaSettings, group, assetEntry.parentGroup }, "Add to group"); aaSettings.MoveEntry(assetEntry, group, MarkEntriesReadOnly); } return assetEntry; } /// /// Returns the name of the group that the asset will be moved to when calling . /// /// /// /// /// public virtual string GetExpectedGroupName(IList locales, Object asset, AddressableAssetSettings aaSettings) { if (locales == null || locales.Count == 0) return GetExpectedSharedGroupName(locales, asset, aaSettings); Locale locale; if (asset is Locale l && l.Identifier == locales[0]) locale = l; else locale = LocalizationEditorSettings.GetLocale(locales[0].Code) ?? Locale.CreateLocale(locales[0]); var expectedGroupPair = m_LocaleGroups.FirstOrDefault(g => g.localeIdentifier == locales[0]); var expectedGroupName = expectedGroupPair?.group != null ? expectedGroupPair.group.Name : Smart.Format(LocaleGroupNamePattern, locale, asset); for (var i = 1; i < locales.Count; ++i) { var groupPair = m_LocaleGroups.FirstOrDefault(g => g.localeIdentifier == locales[i]); locale = LocalizationEditorSettings.GetLocale(locales[i]); var groupName = groupPair?.group != null ? groupPair.group.Name : Smart.Format(LocaleGroupNamePattern, locale, asset); if (expectedGroupName != groupName) { // Use shared group return GetExpectedSharedGroupName(locales, asset, aaSettings); } } return expectedGroupName; } /// /// Returns the name that the Shared group is expected to have. /// /// /// /// /// protected virtual string GetExpectedSharedGroupName(IList locales, Object asset, AddressableAssetSettings aaSettings) { return SharedGroup != null ? SharedGroup.Name : SharedGroupName; } /// /// Returns the Addressable group for the asset. /// /// The locales that depend on the asset. /// The asset that is to be added to an Addressable group. /// The Addressable asset settings. /// Should an Undo record be created if changes are made? /// protected virtual AddressableAssetGroup GetGroup(IList locales, Object asset, AddressableAssetSettings aaSettings, bool createUndo) { var groupName = GetExpectedGroupName(locales, asset, aaSettings); return FindOrCreateGroup(groupName, aaSettings, createUndo); } AddressableAssetGroup FindOrCreateGroup(string name, AddressableAssetSettings aaSettings, bool createUndo) => aaSettings.FindGroup(name) ?? CreateNewGroup(name, MarkEntriesReadOnly, aaSettings, createUndo); static AddressableAssetGroup CreateNewGroup(string name, bool readOnly, AddressableAssetSettings aaSettings, bool createUndo) { if (createUndo) Undo.RecordObject(aaSettings, "Create group"); var group = aaSettings.CreateGroup(name, false, readOnly, true, null, typeof(BundledAssetGroupSchema), typeof(ContentUpdateGroupSchema)); var schema = group.GetSchema(); // Don't use hash as it creates very long file names that can cause issues on Windows. schema.BundleNaming = BundledAssetGroupSchema.BundleNamingStyle.NoHash; if (createUndo) Undo.RegisterCreatedObjectUndo(group, "Create group"); return group; } AddressableAssetEntry GetAssetEntry(Object asset, AddressableAssetSettings aaSettings) => aaSettings.FindAssetEntry(GetAssetGuid(asset)); static string GetAssetGuid(Object asset) { if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(asset, out var guid, out long _)) return guid; Debug.LogError("Failed to extract the asset Guid for " + asset.name, asset); return null; } } }