#if ENABLE_CCD using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Threading.Tasks; using Unity.Services.Ccd.Management; using Unity.Services.Ccd.Management.Models; using Unity.Services.Core; using UnityEditor.AddressableAssets.Settings; using UnityEditor.AddressableAssets.Settings.GroupSchemas; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.AddressableAssets.ResourceLocators; namespace UnityEditor.AddressableAssets.Build { /// /// CCD Events used for building Addressables content with CCD. /// public class CcdBuildEvents { static CcdBuildEvents s_Instance; /// /// The static instance of CcdBuildEvents. /// public static CcdBuildEvents Instance { get { if (s_Instance == null) { s_Instance = new CcdBuildEvents(); s_Instance.RegisterNewBuildEvents(); s_Instance.RegisterUpdateBuildEvents(); } return s_Instance; } } const string k_ContentStatePath = "addressables_content_state.bin"; internal void RegisterNewBuildEvents() { OnPreBuildEvents += s_Instance.VerifyBuildVersion; OnPreBuildEvents += s_Instance.RefreshDataSources; OnPreBuildEvents += s_Instance.VerifyTargetBucket; OnPostBuildEvents += s_Instance.UploadContentState; OnPostBuildEvents += s_Instance.UploadAndRelease; } internal void RegisterUpdateBuildEvents() { OnPreUpdateEvents += s_Instance.VerifyBuildVersion; OnPreUpdateEvents += s_Instance.RefreshDataSources; OnPreUpdateEvents += s_Instance.DownloadContentStateBin; OnPreUpdateEvents += s_Instance.VerifyTargetBucket; OnPostUpdateEvents += s_Instance.UploadContentState; OnPostUpdateEvents += s_Instance.UploadAndRelease; } /// /// Pre Addressables build event. /// public delegate Task PreEvent(AddressablesDataBuilderInput input); /// /// Pre new build events. /// Default events: /// /// /// public static event PreEvent OnPreBuildEvents; /// /// Pre update build events. /// Default events: /// /// /// /// public static event PreEvent OnPreUpdateEvents; /// /// Post Addressables build event. /// public delegate Task PostEvent(AddressablesDataBuilderInput input, AddressablesPlayerBuildResult result); /// /// Post new build events. /// Default events: /// /// /// public static event PostEvent OnPostBuildEvents; /// /// Post update build events. /// Default events: /// /// /// public static event PostEvent OnPostUpdateEvents; internal async Task OnPreEvent(bool isUpdate, AddressablesDataBuilderInput input) { if (isUpdate) { return await InvokePreEvent(OnPreUpdateEvents, input); } return await InvokePreEvent(OnPreBuildEvents, input); } internal async Task InvokePreEvent(PreEvent events, AddressablesDataBuilderInput input) { if (events == null) { return true; } var total = events.GetInvocationList().Length; for (var i = 0; i < total; i++) { var e = (PreEvent)events.GetInvocationList()[i]; var shouldContinue = await e.Invoke(input); if (!shouldContinue) { return false; } } return true; } internal async Task OnPostEvent(bool isUpdate, AddressablesDataBuilderInput input, AddressablesPlayerBuildResult result) { if (isUpdate) { return await InvokePostEvent(OnPostUpdateEvents, input, result); } return await InvokePostEvent(OnPostBuildEvents, input, result); } internal async Task InvokePostEvent(PostEvent events, AddressablesDataBuilderInput input, AddressablesPlayerBuildResult result) { if (events == null) return true; var total = events.GetInvocationList().Length; for (var i = 0; i < total; i++) { var e = (PostEvent)events.GetInvocationList()[i]; var shouldContinue = await e.Invoke(input, result); if (!shouldContinue) { // if a post-build step adds an error we have to log it manually if (result != null && !string.IsNullOrEmpty(result.Error)) { Addressables.LogError(result.Error); } return false; } } return true; } /// /// Prepend an event to the pre new build events. /// /// Pre build event public static void PrependPreBuildEvent(PreEvent newEvent) { Delegate[] oldEvents = OnPreBuildEvents?.GetInvocationList(); OnPreBuildEvents = newEvent; if (oldEvents != null) foreach (var t in oldEvents) { OnPreBuildEvents += (PreEvent)(t); } } /// /// Prepend an event to the post new build events. /// /// Post build event public static void PrependPostBuildEvent(PostEvent newEvent) { Delegate[] oldEvents = OnPostBuildEvents?.GetInvocationList(); OnPostBuildEvents = newEvent; if (oldEvents != null) foreach (var t in oldEvents) { OnPostBuildEvents += (PostEvent)(t); } } /// /// Prepend an event to the pre update build events. /// /// Pre build event public static void PrependPreUpdateEvent(PreEvent newEvent) { Delegate[] oldEvents = OnPreUpdateEvents?.GetInvocationList(); OnPreUpdateEvents = newEvent; if (oldEvents != null) foreach (var t in oldEvents) { OnPreUpdateEvents += (PreEvent)(t); } } /// /// Prepend an event to the post update build events. /// /// Post build event public static void PrependPostUpdateEvent(PostEvent newEvent) { Delegate[] oldEvents = OnPostUpdateEvents?.GetInvocationList(); OnPostUpdateEvents = newEvent; if (oldEvents != null) foreach (var t in oldEvents) { OnPostUpdateEvents += (PostEvent)(t); } } internal void ConfigureCcdManagement(AddressableAssetSettings settings, string environmentId) { CcdManagement.SetEnvironmentId(environmentId); #if CCD_REQUEST_LOGGING CcdManagement.LogRequests = settings.CCDLogRequests; CcdManagement.LogRequestHeaders = settings.CCDLogRequestHeaders; #endif } public Task VerifyBuildVersion(AddressablesDataBuilderInput input) { if (string.IsNullOrWhiteSpace(input.AddressableSettings.OverridePlayerVersion)) { Addressables.LogWarning("When using CCD it is recommended that you set a 'Player Version Override' in Addressables Settings. You can have it use the Player build version by setting it to [UnityEditor.PlayerSettings.bundleVersion]."); Addressables.LogWarning("Documentation on how to disable this warning is available in the example DisableBuildWarnings.cs."); } return Task.FromResult(true); } /// /// Update the CCD data source settings. /// /// Addressables data builder context /// public async Task RefreshDataSources(AddressablesDataBuilderInput input) { return await RefreshDataSources(); } internal async Task RefreshDataSources() { try { var projectId = CloudProjectSettings.projectId; await ProfileDataSourceSettings.UpdateCCDDataSourcesAsync(projectId, true); } catch (Exception e) { Addressables.LogError(e.ToString()); return false; } return true; } internal async Task LoopGroups(AddressableAssetSettings settings, Func> action) { var tasks = new List>(); foreach (var group in settings.groups) { if (group == null) { continue; } var schema = group.GetSchema(); if (schema == null) { continue; } tasks.Add(action(settings, group, schema)); } var results = await Task.WhenAll(tasks.ToArray()); foreach (var result in results) { // if any fail, all fail if (result == false) { return false; } } return true; } /// /// Verify that the targeted CCD bucket exists or create it. /// /// Addressables data builder context /// public async Task VerifyTargetBucket(AddressablesDataBuilderInput input) { try { if (input.AddressableSettings == null) { string error; if (EditorApplication.isUpdating) error = "Addressable Asset Settings does not exist. EditorApplication.isUpdating was true."; else if (EditorApplication.isCompiling) error = "Addressable Asset Settings does not exist. EditorApplication.isCompiling was true."; else error = "Addressable Asset Settings does not exist. Failed to create."; Addressables.LogError(error); return false; } if (!hasRemoteGroups(input.AddressableSettings)) { Addressables.LogWarning("No Addressable Asset Groups have been marked remote or the current profile is not using CCD."); if (input.AddressableSettings.BuildRemoteCatalog) { Addressables.LogWarning("A remote catalog will be built without any remote Asset Bundles."); } } if (input.AddressableSettings.BuildRemoteCatalog) { var dataSource = getRemoteCatalogDataSource(input.AddressableSettings); var success = await verifyTargetBucket(input.AddressableSettings, "Remote Catalog", dataSource); if (!success) { return false; } } // Reclean directory before every build if (Directory.Exists(AddressableAssetSettings.kCCDBuildDataPath)) { Directory.Delete(AddressableAssetSettings.kCCDBuildDataPath, true); } } catch (Exception e) { Addressables.LogError($"Unable to verify target bucket: {e.Message}"); return false; } return await LoopGroups(input.AddressableSettings, verifyTargetBucket); } internal bool hasRemoteGroups(AddressableAssetSettings settings) { foreach (var group in settings.groups) { if (group == null) { continue; } var schema = group.GetSchema(); if (schema == null) { continue; } var dataSource = GetDataSource(settings, schema); if (isCCDGroup(dataSource)) { return true; } } return false; } internal bool isCCDGroup(ProfileGroupType dataSource) { if (dataSource == null) { return false; } if (IsUsingManager(dataSource)) { return true; } return dataSource.GroupTypePrefix.StartsWith("CCD"); } internal async Task verifyTargetBucket(AddressableAssetSettings settings, AddressableAssetGroup group, BundledAssetGroupSchema schema) { AddressableAssetSettings.NullifyBundleFileIds(group); var dataSource = GetDataSource(settings, schema); return await verifyTargetBucket(settings, group.Name, dataSource); } internal async Task verifyTargetBucket(AddressableAssetSettings settings, string groupName, ProfileGroupType dataSource) { try { // if not using the manager try to lookup the bucket and verify it's not promotion only if (!IsUsingManager(dataSource)) { if (dataSource == null) { return true; } var promotionOnly = IsPromotionOnlyBucket(dataSource); if (promotionOnly) { Addressables.LogError("Cannot upload to Promotion Only bucket."); return false; } return true; } // CcdManagedData.ConfigState.Override means it has been overriden by the customer at build time if (settings.m_CcdManagedData.State == CcdManagedData.ConfigState.Override) { return true; } if (settings.m_CcdManagedData.IsConfigured()) { // this has been configured by a previous run return true; } // existing automatic bucket loaded from cache var bucketIdVariable = dataSource .GetVariableBySuffix($"{nameof(CcdBucket)}{nameof(CcdBucket.Id)}"); if (bucketIdVariable != null) { var promotionOnly = IsPromotionOnlyBucket(dataSource); if (promotionOnly) { Debug.LogError("Cannot upload to Promotion Only bucket."); return false; } PopulateCcdManagedData(settings, settings.activeProfileId); return true; } // otherwise try to create var environmentId = ProfileDataSourceSettings.GetSettings().GetEnvironmentId(settings.profileSettings, settings.activeProfileId); CcdManagement.SetEnvironmentId(environmentId); // this should be getting the value from the active profile var ccdBucket = await CreateManagedBucket(EditorUserBuildSettings.activeBuildTarget.ToString()); if (ccdBucket == null) { // the bucket already exists, we shouldn't be here if refresh was called ccdBucket = await GetExistingManagedBucket(); } var environmentName = ProfileDataSourceSettings.GetSettings().GetEnvironmentName(settings.profileSettings, settings.activeProfileId); ProfileDataSourceSettings.AddGroupTypeForRemoteBucket(CloudProjectSettings.projectId, environmentId, environmentName, ccdBucket, new List()); PopulateCcdManagedData(settings, settings.activeProfileId); // I should put this value into the data source list return true; } catch (Exception e) { Addressables.LogError($"Unable to verify target bucket for {groupName}: {e.Message}"); return false; } } public void PopulateCcdManagedData(AddressableAssetSettings settings, string profileId) { // reset the state data settings.m_CcdManagedData = new CcdManagedData(); var buildPath = settings.profileSettings.GetVariableId(AddressableAssetSettings.kRemoteBuildPath); var loadPath = settings.profileSettings.GetVariableId(AddressableAssetSettings.kRemoteLoadPath); if (buildPath == null || loadPath == null) { Addressables.Log($"Not populating CCD managed data. No remote paths are configured for profile {settings.profileSettings.GetProfileName(profileId)}."); return; } var dataSource = GetDataSource(settings, buildPath, loadPath); if (dataSource == null) { Addressables.Log($"Not populating CCD managed data. Data source not found. Try refreshing data sources in the profile window."); return; } if (!IsUsingManager(dataSource)) { return; } var bucketIdVariable = dataSource .GetVariableBySuffix($"{nameof(CcdBucket)}{nameof(CcdBucket.Id)}"); if (bucketIdVariable == null) { Addressables.Log("Not populating CCD managed data. No bucket ID found. Try refreshing data sources in the profile window."); return; } settings.m_CcdManagedData.BucketId = bucketIdVariable.Value; settings.m_CcdManagedData.Badge = dataSource .GetVariableBySuffix($"{nameof(CcdBadge)}{nameof(CcdBadge.Name)}").Value; // Target bucket should only be verified if Automatic profile type settings.m_CcdManagedData.EnvironmentId = ProfileDataSourceSettings.GetSettings().GetEnvironmentId(settings.profileSettings, profileId); settings.m_CcdManagedData.EnvironmentName = ProfileDataSourceSettings.GetSettings().GetEnvironmentName(settings.profileSettings, profileId); } /// /// Download addressables_content_state.bin from the CCD managed bucket. /// /// Addressables data builder context /// public async Task DownloadContentStateBin(AddressablesDataBuilderInput input) { if (!input.AddressableSettings.BuildRemoteCatalog) { Addressables.LogWarning("Not downloading content state because 'Build Remote Catalog' is not checked in Addressable Asset Settings. This will disable content updates"); return true; } var settings = input.AddressableSettings; var dataSource = getRemoteCatalogDataSource(settings); if (dataSource == null || !this.isCCDGroup(dataSource)) { Addressables.LogError("Content state could not be downloaded as the remote catalog is not targeting CCD"); return false; } try { SetEnvironmentId(settings, dataSource); var bucketId = GetBucketId(settings, dataSource); if (bucketId == null) { Addressables.LogError("Content state could not be downloaded as no bucket was specified. This is populated for managed profiles in the VerifyTargetBucket event."); return false; } var api = CcdManagement.Instance; CcdEntry ccdEntry; try { ccdEntry = await GetEntryByPath(api, new Guid(bucketId), k_ContentStatePath); } catch (Exception e) { Addressables.LogError($"Unable to get entry for content state {k_ContentStatePath}: {e.Message}"); return false; } if (ccdEntry != null) { var contentStream = await api.GetContentAsync(new EntryOptions(new Guid(bucketId), ccdEntry.Entryid)); var contentStatePath = Path.Combine(settings.GetContentStateBuildPath(), k_ContentStatePath); if (!Directory.Exists(contentStatePath)) Directory.CreateDirectory(Path.GetDirectoryName(contentStatePath)); else if (File.Exists(contentStatePath)) File.Delete(contentStatePath); using (var fileStream = File.Create(contentStatePath)) { contentStream.CopyTo(fileStream); } } } catch (Exception e) { Addressables.LogError($"Unable to upload content state {k_ContentStatePath}: {e.Message}"); return false; } return true; } /// /// Upload content to the CCD managed bucket and create a release. /// /// Addressables data builder context /// Addressables build result /// public async Task UploadAndRelease(AddressablesDataBuilderInput input, AddressablesPlayerBuildResult result) { // Verify files exist that need uploading var foundRemoteContent = result.FileRegistry?.GetFilePaths() .Any(path => path.StartsWith(AddressableAssetSettings.kCCDBuildDataPath)) == true; if (!foundRemoteContent) { Addressables.LogWarning( "Skipping upload and release as no remote content was found to upload. Ensure you have at least one content group's 'Build & Load Path' set to Remote."); return false; } try { //Getting files Addressables.Log("Creating and uploading entries"); var startDirectory = new DirectoryInfo(AddressableAssetSettings.kCCDBuildDataPath); var buildData = CreateData(startDirectory); //Creating a release for each bucket var defaultEnvironmentId = ProfileDataSourceSettings.GetSettings().GetEnvironmentId(input.AddressableSettings.profileSettings, input.AddressableSettings.activeProfileId); await UploadAndRelease(CcdManagement.Instance, input.AddressableSettings, defaultEnvironmentId, buildData); } catch (Exception e) { Addressables.LogError(e.ToString()); return false; } return true; } private ProfileGroupType getRemoteCatalogDataSource(AddressableAssetSettings settings) { var buildPath = settings.RemoteCatalogBuildPath; var loadPath = settings.RemoteCatalogLoadPath; return GetDataSource(settings, buildPath.Id, loadPath.Id); } /// /// Upload addressables_content_state.bin to the CCD managed bucket. /// /// Addressables data builder context /// Addressables build result /// public async Task UploadContentState(AddressablesDataBuilderInput input, AddressablesPlayerBuildResult result) { if (!input.AddressableSettings.BuildRemoteCatalog) { Addressables.LogWarning("Not uploading content state, because 'Build Remote Catalog' is not checked in Addressable Asset Settings. This will disable content updates"); return true; } var settings = input.AddressableSettings; var dataSource = getRemoteCatalogDataSource(settings); if (dataSource == null || !this.isCCDGroup(dataSource)) { Addressables.LogError("Content state could not be uploaded as the remote catalog is not targeting CCD"); return false; } try { SetEnvironmentId(settings, dataSource); var bucketId = GetBucketId(settings, dataSource); if (bucketId == null) { Addressables.LogError("Content state could not be uploaded as no bucket was specified. This is populated for managed profiles in the VerifyBucket event."); return false; } var api = CcdManagement.Instance; var contentStatePath = Path.Combine(settings.GetContentStateBuildPath(), k_ContentStatePath); if (!File.Exists(contentStatePath)) { Addressables.LogError($"Content state file is missing {contentStatePath}"); return false; } var contentHash = AddressableAssetUtility.GetMd5Hash(contentStatePath); using (var stream = File.OpenRead(contentStatePath)) { var entryModelOptions = new EntryModelOptions(k_ContentStatePath, contentHash, (int)stream.Length) { UpdateIfExists = true }; CcdEntry createdEntry; try { createdEntry = await api.CreateOrUpdateEntryByPathAsync( new EntryByPathOptions(new Guid(bucketId), k_ContentStatePath), entryModelOptions); } catch (Exception e) { Addressables.LogError($"Unable to create entry for content state: {e.Message}"); return false; } try { var uploadContentOptions = new UploadContentOptions( new Guid(bucketId), createdEntry.Entryid, stream); await api.UploadContentAsync(uploadContentOptions); } catch (Exception e) { Addressables.LogError($"Unable to upload content state: {e.Message}"); return false; } } } catch (Exception e) { Addressables.LogError(e.ToString()); return false; } return true; } internal bool IsPromotionOnlyBucket(ProfileGroupType dataSource) { if (dataSource != null && dataSource.GroupTypePrefix.StartsWith("CCD")) { if (bool.Parse(dataSource.GetVariableBySuffix(nameof(CcdBucket.Attributes.PromoteOnly)).Value)) { Addressables.LogError("Cannot upload to Promotion Only bucket."); return true; } } return false; } internal ProfileGroupType GetDataSource(AddressableAssetSettings settings, BundledAssetGroupSchema schema) { return GetDataSource(settings, schema.BuildPath.Id, schema.LoadPath.Id); } internal ProfileGroupType GetDataSource(AddressableAssetSettings settings, string buildPathId, string loadPathId) { var groupType = GetGroupType(settings, buildPathId, loadPathId); if (!IsUsingManager(groupType)) { return groupType; } var environmentId = ProfileDataSourceSettings.GetSettings().GetEnvironmentId(settings.profileSettings, settings.activeProfileId); // if we haven't setup an automatic group since refresh we do it here IEnumerable groupTypes = ProfileDataSourceSettings.GetSettings().GetGroupTypesByPrefix(string.Join( ProfileGroupType.k_PrefixSeparator.ToString(), "CCD", CloudProjectSettings.projectId, ProfileDataSourceSettings.GetSettings().GetEnvironmentId(settings.profileSettings, settings.activeProfileId))); // if we have setup an automatic group we load it here groupTypes = groupTypes.Concat(ProfileDataSourceSettings.GetSettings().GetGroupTypesByPrefix(AddressableAssetSettings.CcdManagerGroupTypePrefix)); var automaticGroupType = groupTypes.FirstOrDefault(gt => gt.GetVariableBySuffix($"{nameof(CcdBucket)}{nameof(CcdBucket.Name)}")?.Value == EditorUserBuildSettings.activeBuildTarget.ToString() && gt.GetVariableBySuffix($"{nameof(ProfileDataSourceSettings.Environment)}{nameof(ProfileDataSourceSettings.Environment.id)}")?.Value == environmentId); if (automaticGroupType == null) { // the bucket does not yet exist return groupType; } // set this value so we can check with IsUsingManager automaticGroupType.GroupTypePrefix = AddressableAssetSettings.CcdManagerGroupTypePrefix; return automaticGroupType; } internal ProfileGroupType GetGroupType(AddressableAssetSettings settings, string buildPathId, string loadPathId) { // This data is populated in the RefreshDataSources event // we need the "unresolved" value since we're tring to match it to its original type var buildPathValue = settings.profileSettings.GetValueById(settings.activeProfileId, buildPathId); var loadPathValue = settings.profileSettings.GetValueById(settings.activeProfileId, loadPathId); if (buildPathValue == null || loadPathValue == null) { return null; } var tempGroupType = new ProfileGroupType("temp"); tempGroupType.AddVariable(new ProfileGroupType.GroupTypeVariable(AddressableAssetSettings.kBuildPath, buildPathValue)); tempGroupType.AddVariable(new ProfileGroupType.GroupTypeVariable(AddressableAssetSettings.kLoadPath, loadPathValue)); return ProfileDataSourceSettings.GetSettings().FindGroupType(tempGroupType); } internal bool IsUsingManager(ProfileGroupType dataSource) { if (dataSource == null) { return false; } return dataSource.GroupTypePrefix == AddressableAssetSettings.CcdManagerGroupTypePrefix; } internal void SetEnvironmentId(AddressableAssetSettings settings, ProfileGroupType groupType) { string environmentId = null; if (!IsUsingManager(groupType)) { // if not using the manager load the bucketID from the group type environmentId = groupType.GetVariableBySuffix($"{nameof(ProfileDataSourceSettings.Environment)}{nameof(ProfileDataSourceSettings.Environment.id)}").Value; } else if (settings.m_CcdManagedData != null) { environmentId = settings.m_CcdManagedData.EnvironmentId; } if (environmentId == null) { throw new Exception("unable to determine environment ID."); } ConfigureCcdManagement(settings, environmentId); } internal string GetBucketId(AddressableAssetSettings settings, ProfileGroupType dataSource) { if (!IsUsingManager(dataSource)) { // if not using the manager load the bucketID from the group type return dataSource.GetVariableBySuffix($"{nameof(CcdBucket)}{nameof(CcdBucket.Id)}").Value; } if (settings.m_CcdManagedData != null) { return settings.m_CcdManagedData.BucketId; } return null; } async Task CreateManagedBucket(string bucketName) { CcdBucket ccdBucket; try { ccdBucket = await CcdManagement.Instance.CreateBucketAsync( new CreateBucketOptions(bucketName)); } catch (CcdManagementException e) { if (e.ErrorCode == CcdManagementErrorCodes.AlreadyExists) { return null; } else { throw; } } return ccdBucket; } async Task GetExistingManagedBucket() { var buckets = await ProfileDataSourceSettings.GetAllBucketsAsync(); var ccdBucket = buckets.First(bucket => bucket.Value.Name == EditorUserBuildSettings.activeBuildTarget.ToString()).Value; return ccdBucket; } async Task GetEntryByPath(ICcdManagementServiceSdk api, Guid bucketId, string path) { CcdEntry ccdEntry = null; try { ccdEntry = await api.GetEntryByPathAsync(new EntryByPathOptions(bucketId, path)); } catch (CcdManagementException e) { if (e.ErrorCode != CommonErrorCodes.NotFound) { throw; } } return ccdEntry; } CcdBuildDataFolder CreateData(DirectoryInfo startDirectory) { var buildDataFolder = new CcdBuildDataFolder { Name = AddressableAssetSettings.kCCDBuildDataPath, Location = startDirectory.FullName }; buildDataFolder.GetChildren(startDirectory); return buildDataFolder; } int StartProgress(string description) { return Progress.Start("CCD", description, Progress.Options.Managed); } void RemoveProgress(int progressId) { Progress.Remove(progressId); } void ReportProgress(int progressId, float progress, string message) { Progress.Report(progressId, progress, message); } async Task UploadAndRelease(ICcdManagementServiceSdk api, AddressableAssetSettings settings, string defaultEnvironmentId, CcdBuildDataFolder buildData) { var progressId = StartProgress("Upload and Release"); try { foreach (var env in buildData.Environments) { CcdManagement.SetEnvironmentId(env.Name); if (env.Name == ProfileDataSourceSettings.MANAGED_ENVIRONMENT) { ConfigureCcdManagement(settings, defaultEnvironmentId); } foreach (var bucket in env.Buckets) { Guid bucketId; var bucketIdString = bucket.Name == ProfileDataSourceSettings.MANAGED_BUCKET ? settings.m_CcdManagedData.BucketId : bucket.Name; if (String.IsNullOrEmpty(bucketIdString)) { Addressables.LogError($"Invalid bucket ID for {bucket.Name}"); continue; } bucketId = Guid.Parse(bucketIdString); foreach (var badge in bucket.Badges) { if (badge.Name == ProfileDataSourceSettings.MANAGED_BADGE) { badge.Name = "latest"; } var entries = new List(); var total = badge.Files.Count(); for (var i = 0; i < total; i++) { var file = badge.Files[i]; var contentHash = AddressableAssetUtility.GetMd5Hash(file.FullName); using (var stream = File.OpenRead(file.FullName)) { var entryPath = file.Name; var entryModelOptions = new EntryModelOptions(entryPath, contentHash, (int)stream.Length) { UpdateIfExists = true }; ReportProgress(progressId, (i + 1) / total, $"Creating Entry {entryPath}"); CcdEntry createdEntry; try { createdEntry = await api.CreateOrUpdateEntryByPathAsync(new EntryByPathOptions(bucketId, entryPath), entryModelOptions).ConfigureAwait(false); } catch (Exception e) { throw new Exception($"Unable to create entry for {entryPath}: {e.Message}", e); } Addressables.Log($"Created Entry {entryPath}"); ReportProgress(progressId, (i + 1) / total, $"Uploading Entry {entryPath}"); var uploadContentOptions = new UploadContentOptions(bucketId, createdEntry.Entryid, stream); await api.UploadContentAsync(uploadContentOptions).ConfigureAwait(false); ReportProgress(progressId, (i + 1) / total, $"Uploaded Entry {entryPath}"); entries.Add(new CcdReleaseEntryCreate(createdEntry.Entryid, createdEntry.CurrentVersionid)); } } // Add content_sate.bin to release if present var contentStateEntry = await GetEntryByPath(api, bucketId, k_ContentStatePath); if (contentStateEntry != null) entries.Add(new CcdReleaseEntryCreate(contentStateEntry.Entryid, contentStateEntry.CurrentVersionid)); //Creating release ReportProgress(progressId, total, "Creating release"); Addressables.Log("Creating release."); var release = await api.CreateReleaseAsync(new CreateReleaseOptions(bucketId) { Entries = entries, Notes = $"Automated release created for {badge.Name}" }).ConfigureAwait(false); Addressables.Log($"Release {release.Releaseid} created."); //Don't update latest badge (as it always updates) if (badge.Name != "latest") { ReportProgress(progressId, total, "Updating badge"); Addressables.Log("Updating badge."); var badgeRes = await api.AssignBadgeAsync(new AssignBadgeOptions(bucketId, badge.Name, release.Releaseid)) .ConfigureAwait(false); Addressables.Log($"Badge {badgeRes.Name} updated."); } } } } } finally { RemoveProgress(progressId); } } } } #endif