using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using UnityEngine; using UnityEngine.AddressableAssets; #if (ENABLE_CCD && UNITY_2019_4_OR_NEWER) using Unity.Services.CCD.Management; using Unity.Services.CCD.Management.Apis.Badges; using Unity.Services.CCD.Management.Apis.Buckets; using Unity.Services.CCD.Management.Badges; using Unity.Services.CCD.Management.Buckets; using Unity.Services.CCD.Management.Http; using Unity.Services.CCD.Management.Models; #endif namespace UnityEditor.AddressableAssets.Settings { /// /// Scriptable Object that holds data source setting information for the profile data source dropdown window /// public class ProfileDataSourceSettings : ScriptableObject, ISerializationCallbackReceiver { const string DEFAULT_PATH = "Assets/AddressableAssetsData"; const string DEFAULT_NAME = "ProfileDataSourceSettings"; const string CONTENT_RANGE_HEADER = "Content-Range"; static string DEFAULT_SETTING_PATH = $"{DEFAULT_PATH}/{DEFAULT_NAME}.asset"; /// /// Group types that exist within the settings object /// [SerializeField] public List profileGroupTypes = new List(); /// /// Creates, if needed, and returns the profile data source settings for the project /// /// Desired path to put settings /// Desired name for settings /// public static ProfileDataSourceSettings Create(string path = null, string settingName = null) { ProfileDataSourceSettings aa; var assetPath = DEFAULT_SETTING_PATH; if (path != null && settingName != null) { assetPath = $"{path}/{settingName}.asset"; } aa = AssetDatabase.LoadAssetAtPath(assetPath); if (aa == null) { Directory.CreateDirectory(path != null ? path : DEFAULT_PATH); aa = CreateInstance(); AssetDatabase.CreateAsset(aa, assetPath); aa = AssetDatabase.LoadAssetAtPath(assetPath); aa.profileGroupTypes = CreateDefaultGroupTypes(); EditorUtility.SetDirty(aa); } return aa; } /// /// Gets the profile data source settings for the project /// /// /// /// public static ProfileDataSourceSettings GetSettings(string path = null, string settingName = null) { ProfileDataSourceSettings aa; var assetPath = DEFAULT_SETTING_PATH; if (path != null && settingName != null) { assetPath = $"{path}/{settingName}.asset"; } aa = AssetDatabase.LoadAssetAtPath(assetPath); if (aa == null) return Create(); return aa; } /// /// Creates a list of default group types that are automatically added on ProfileDataSourceSettings object creation /// /// List of ProfileGroupTypes: Built-In and Editor Hosted public static List CreateDefaultGroupTypes() => new List{CreateBuiltInGroupType(), CreateEditorHostedGroupType()}; static ProfileGroupType CreateBuiltInGroupType() { ProfileGroupType defaultBuiltIn = new ProfileGroupType(AddressableAssetSettings.LocalGroupTypePrefix); defaultBuiltIn.AddVariable(new ProfileGroupType.GroupTypeVariable(AddressableAssetSettings.kBuildPath, AddressableAssetSettings.kLocalBuildPathValue)); defaultBuiltIn.AddVariable(new ProfileGroupType.GroupTypeVariable(AddressableAssetSettings.kLoadPath, AddressableAssetSettings.kLocalLoadPathValue)); return defaultBuiltIn; } static ProfileGroupType CreateEditorHostedGroupType() { ProfileGroupType defaultRemote = new ProfileGroupType(AddressableAssetSettings.EditorHostedGroupTypePrefix); defaultRemote.AddVariable(new ProfileGroupType.GroupTypeVariable(AddressableAssetSettings.kBuildPath, AddressableAssetSettings.kRemoteBuildPathValue)); defaultRemote.AddVariable(new ProfileGroupType.GroupTypeVariable(AddressableAssetSettings.kLoadPath, AddressableAssetSettings.RemoteLoadPathValue)); return defaultRemote; } /// /// Given a valid profileGroupType, searches the settings and returns, if exists, the profile group type /// /// /// ProfileGroupType if found, null otherwise public ProfileGroupType FindGroupType(ProfileGroupType groupType) { ProfileGroupType result = null; if (!groupType.IsValidGroupType()) { throw new ArgumentException("Group Type is not valid. Group Type must include a build path and load path variables"); } foreach (ProfileGroupType settingsGroupType in profileGroupTypes) { var buildPath = groupType.GetVariableBySuffix(AddressableAssetSettings.kBuildPath); var foundBuildPath = settingsGroupType.ContainsVariable(buildPath); var loadPath = groupType.GetVariableBySuffix(AddressableAssetSettings.kLoadPath); var foundLoadPath = settingsGroupType.ContainsVariable(loadPath); if (foundBuildPath && foundLoadPath) { result = settingsGroupType; break; } } return result; } /// /// Retrieves a list of ProfileGroupType that matches the given prefix /// /// prefix to search by /// List of ProfileGroupType public List GetGroupTypesByPrefix(string prefix) { return profileGroupTypes.Where((groupType) => groupType.GroupTypePrefix.StartsWith(prefix)).ToList(); } #if (ENABLE_CCD && UNITY_2019_4_OR_NEWER) /// /// Updates the CCD buckets and badges with the data source settings /// /// Project Id connected to Unity Services /// Whether or not to show debug logs or not /// List of ProfileGroupType public static async Task> UpdateCCDDataSourcesAsync(string projectId, bool showInfoLog) { if (showInfoLog) Addressables.Log("Syncing CCD Buckets and Badges."); var settings = GetSettings(); var profileGroupTypes = new List(); profileGroupTypes.AddRange(CreateDefaultGroupTypes()); await CCDManagementAPIService.SetConfigurationAuthHeader(CloudProjectSettings.accessToken); var bucketDictionary = await GetAllBucketsAsync(projectId); foreach (var kvp in bucketDictionary) { var bucket = kvp.Value; var badges = await GetAllBadgesAsync(projectId, bucket.Id.ToString()); if (badges.Count == 0) badges.Add(new CcdBadge(name: "latest")); foreach (var badge in badges) { var groupType = new ProfileGroupType($"CCD{ProfileGroupType.k_PrefixSeparator}{projectId}{ProfileGroupType.k_PrefixSeparator}{bucket.Id}{ProfileGroupType.k_PrefixSeparator}{badge.Name}"); groupType.AddVariable(new ProfileGroupType.GroupTypeVariable($"{nameof(CcdBucket)}{nameof(CcdBucket.Name)}", bucket.Name)); groupType.AddVariable(new ProfileGroupType.GroupTypeVariable($"{nameof(CcdBucket)}{nameof(CcdBucket.Id)}", bucket.Id.ToString())); groupType.AddVariable(new ProfileGroupType.GroupTypeVariable($"{nameof(CcdBadge)}{nameof(CcdBadge.Name)}", badge.Name)); groupType.AddVariable(new ProfileGroupType.GroupTypeVariable(nameof(CcdBucket.Attributes.PromoteOnly), bucket.Attributes.PromoteOnly.ToString())); string buildPath = $"{AddressableAssetSettings.kCCDBuildDataPath}/{bucket.Id}/{badge.Name}"; groupType.AddVariable(new ProfileGroupType.GroupTypeVariable(AddressableAssetSettings.kBuildPath, buildPath)); string loadPath = $"https://{projectId}.client-api.unity3dusercontent.com/client_api/v1/buckets/{bucket.Id}/release_by_badge/{badge.Name}/entry_by_path/content/?path="; groupType.AddVariable(new ProfileGroupType.GroupTypeVariable(AddressableAssetSettings.kLoadPath, loadPath)); profileGroupTypes.Add(groupType); } } settings.profileGroupTypes = profileGroupTypes; if (showInfoLog) Addressables.Log("Successfully synced CCD Buckets and Badges."); EditorUtility.SetDirty(settings); AddressableAssetUtility.OpenAssetIfUsingVCIntegration(settings); return settings.profileGroupTypes; } private static async Task> GetAllBucketsAsync(string projectId) { int numBuckets = Int32.MaxValue; int page = 1; List buckets = new List(); BucketsApiClient client = new BucketsApiClient(new HttpClient()); do { ListBucketsByProjectRequest request = new ListBucketsByProjectRequest(projectId, page); Response> response = await client.ListBucketsByProjectAsync(request); if (response.Result.Count > 0) buckets.AddRange(response.Result); if (page == 1) { response.Headers.TryGetValue(CONTENT_RANGE_HEADER, out string contentLength); // content-range: items x-y/z => grab z numBuckets = Int32.Parse(contentLength.Split('/')[1]); } page++; } while (buckets.Count < numBuckets); return buckets.ToDictionary(kvp => kvp.Id, kvp => kvp); } private static async Task> GetAllBadgesAsync(string projectId, string bucketId) { int numBadges = Int32.MaxValue; int page = 1; List badges = new List(); BadgesApiClient client = new BadgesApiClient(new HttpClient()); do { ListBadgesRequest request = new ListBadgesRequest(bucketId, projectId, page); Response> response = await client.ListBadgesAsync(request); if (response.Result.Count > 0) badges.AddRange(response.Result); if (page == 1) { response.Headers.TryGetValue(CONTENT_RANGE_HEADER, out string contentLength); // content-range: items x-y/z => grab z numBadges = Int32.Parse(contentLength.Split('/')[1]); } page++; } while (badges.Count < numBadges); return badges; } #endif void ISerializationCallbackReceiver.OnBeforeSerialize() { } void ISerializationCallbackReceiver.OnAfterDeserialize() { // Ensure static Group types have the correct string // Local var types = GetGroupTypesByPrefix(AddressableAssetSettings.LocalGroupTypePrefix); if (types == null || types.Count == 0) profileGroupTypes.Add(CreateBuiltInGroupType()); else { types[0].AddOrUpdateVariable(new ProfileGroupType.GroupTypeVariable(AddressableAssetSettings.kBuildPath, AddressableAssetSettings.kLocalBuildPathValue)); types[0].AddOrUpdateVariable(new ProfileGroupType.GroupTypeVariable(AddressableAssetSettings.kLoadPath, AddressableAssetSettings.kLocalLoadPathValue)); } // Editor Hosted types = GetGroupTypesByPrefix(AddressableAssetSettings.EditorHostedGroupTypePrefix); if (types.Count == 0) profileGroupTypes.Add(CreateEditorHostedGroupType()); else { types[0].AddOrUpdateVariable(new ProfileGroupType.GroupTypeVariable(AddressableAssetSettings.kBuildPath, AddressableAssetSettings.kRemoteBuildPathValue)); types[0].AddOrUpdateVariable(new ProfileGroupType.GroupTypeVariable(AddressableAssetSettings.kLoadPath, AddressableAssetSettings.RemoteLoadPathValue)); } } } }